feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

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

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

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,62 @@
<Grid x:Class="PCL.MyMsgLogin"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL"
xmlns:controls="clr-namespace:PCL.Core.UI.Controls;assembly=PCL.Core"
RenderTransformOrigin="0,0.5" UseLayoutRounding="True" SnapsToDevicePixels="True" MinWidth="400"
HorizontalAlignment="Center" VerticalAlignment="Center" Margin="25">
<Grid.RenderTransform>
<TransformGroup>
<RotateTransform x:Name="TransformRotate" Angle="-4" />
<TranslateTransform x:Name="TransformPos" X="0" Y="40" />
</TransformGroup>
</Grid.RenderTransform>
<controls:BlurBorder Name="PanBorder" CornerRadius="7"
Background="{DynamicResource ColorBrushBackground}"
MouseLeftButtonDown="Drag">
<controls:BlurBorder.Effect>
<DropShadowEffect Color="{DynamicResource ColorObjectMsgBoxShadow}" BlurRadius="20" ShadowDepth="4"
RenderingBias="Performance" Opacity="0.8" x:Name="EffectShadow" />
</controls:BlurBorder.Effect>
<Grid Name="PanMain" VerticalAlignment="Top" Margin="22,22,22,23">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="2" />
<RowDefinition Height="13" />
<RowDefinition Height="1*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="23" TextTrimming="None" Foreground="{DynamicResource ColorBrush2}"
HorizontalAlignment="Left" Name="LabTitle" Margin="7,-1,70,9"
Text="{DynamicResource Launch.Account.LoginDialog.Title}"
VerticalAlignment="Top" SnapsToDevicePixels="False" UseLayoutRounding="False"
MouseLeftButtonDown="Drag" />
<Rectangle x:Name="ShapeLine" Grid.Row="1" Height="2" Fill="{Binding Foreground, ElementName=LabTitle}" />
<local:MyScrollViewer Grid.Row="3" VerticalAlignment="Top" x:Name="PanCaption" Margin="0,0,0,17"
Padding="7,0,15,0"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
DeltaMult="0.7">
<TextBlock VerticalAlignment="Top" TextTrimming="None" TextWrapping="Wrap" Name="LabCaption"
FontSize="15" LineHeight="18"
Foreground="{DynamicResource ColorBrush1}" FontWeight="Normal" Padding="1" />
</local:MyScrollViewer>
<StackPanel Grid.Row="4" Name="PanBtn" VerticalAlignment="Top" HorizontalAlignment="Right"
Margin="150,0,8,0" Orientation="Horizontal">
<local:MyButton ColorType="Normal" x:FieldModifier="public"
Text="{DynamicResource Launch.Account.LoginDialog.ReopenWebpage}" x:Name="Btn1"
Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0"
UseLayoutRounding="False"
local:CustomEventService.EventType="OpenUrl" />
<local:MyButton ColorType="Normal" x:FieldModifier="public"
Text="{DynamicResource Launch.Account.LoginDialog.CopyCode}" x:Name="Btn2"
Margin="12,0,0,0" TextPadding="7" SnapsToDevicePixels="False" Padding="5,0"
UseLayoutRounding="False"
local:CustomEventService.EventType="CopyText" />
<local:MyButton ColorType="Normal" x:FieldModifier="public"
Text="{DynamicResource Common.Action.Cancel}" x:Name="Btn3" Margin="12,0,0,0"
TextPadding="7" SnapsToDevicePixels="False" Padding="5,0" UseLayoutRounding="False"
Click="Btn3_Click" />
</StackPanel>
</Grid>
</controls:BlurBorder>
</Grid>
@@ -0,0 +1,242 @@
using System.Windows.Controls;
using System.Windows.Input;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.UI.Controls;
using PCL.Core.Utils;
using PCL.Core.IO.Net.Http;
using System.Text.Json.Serialization;
namespace PCL;
public partial class MyMsgLogin
{
private readonly JsonObject data;
private string deviceCode; // 用于轮询的设备代码
private string oAuthUrl = ""; // OAuth 轮询验证地址
private string userCode; // 需要用户在网页上输入的设备代码
private string website; // 验证网页的网址
private Task? workingThread;
public MyMsgLogin()
{
InitializeComponent();
// Handles
Loaded += Load;
Btn1.Click += Btn1_Click;
Btn3.Click += Btn3_Click;
PanBorder.MouseLeftButtonDown += Drag;
LabTitle.MouseLeftButtonDown += Drag;
}
private void Finished(object result)
{
if (myConverter.IsExited)
return;
myConverter.IsExited = true;
myConverter.Result = result;
ModBase.RunInUi(Close);
Thread.Sleep(200);
ModMain.frmMain.ShowWindowToTop();
}
private void Init()
{
userCode = (string)data["user_code"];
deviceCode = (string)data["device_code"];
ModBase.ClipboardSet(deviceCode);
if (data["verification_uri_complete"] is not null)
{
website = (string)data["verification_uri_complete"];
LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions.WithAutoFill", userCode, website);
}
else
{
website = (string)data["verification_uri"];
LabCaption.Text = Lang.Text("Launch.Account.LoginDialog.MicrosoftInstructions", userCode, website);
}
// 设置 UI
LabTitle.Text = Lang.Text("Launch.Account.LoginDialog.MinecraftLogin");
CustomEventService.SetEventData(Btn1, website);
CustomEventService.SetEventData(Btn2, userCode);
// 启动工作线程
workingThread = WorkThreadAsync();
}
private record ErrorBody(
[property: JsonPropertyName("error")] string Error,
[property: JsonPropertyName("error_description")] string Desc);
private async Task WorkThreadAsync()
{
await Task.Delay(2000).ConfigureAwait(false);
if (myConverter.IsExited)
return;
ModBase.OpenWebsite(website);
ModBase.ClipboardSet(userCode);
var delayTime = (data["interval"].ToObject<int>() - 1) * 1000;
// 轮询
var unknownFailureCount = 0;
while (!myConverter.IsExited)
{
try
{
var bodyData = $"grant_type=urn:ietf:params:oauth:grant-type:device_code&client_id={Secrets.MSOAuthClientId}&device_code={deviceCode}&scope=XboxLive.signin%20offline_access";
using var result = await HttpRequest
.Create("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
.WithFormContent(bodyData)
.SendAsync(enableLogging: false)
.ConfigureAwait(false);
if (!result.IsSuccess)
{
var error = await result.AsJsonAsync<ErrorBody>()
.ConfigureAwait(false);
switch(error?.Error)
{
case "authorization_pending":
{
await Task.Delay(delayTime)
.ConfigureAwait(false);
continue;
}
default:
{
throw new Exception(error?.Error ?? "Unable to get body");
}
}
}
// 获取结果
var ctx = await result.AsStringAsync().ConfigureAwait(false);
var resultJson = (JsonObject)ModBase.GetJson(ctx);
ModProfile.ProfileLog($"令牌过期时间:{resultJson["expires_in"]} 秒");
HintService.Hint(Lang.Text("Launch.Account.LoginDialog.Success"), HintType.Success);
Finished(new[] { resultJson["access_token"].ToString(), resultJson["refresh_token"].ToString() });
return;
}
catch (Exception ex)
{
if (unknownFailureCount <= 2)
{
unknownFailureCount += 1;
ModBase.Log(ex, $"正版验证轮询第 {unknownFailureCount} 次失败");
ModBase.Log(ex.Message);
await Task.Delay(2000).ConfigureAwait(false);
}
else
{
Finished(new Exception(Lang.Text("Launch.Account.LoginDialog.PollingFailed"), ex));
return;
}
}
}
}
#region
private readonly ModMain.MyMsgBoxConverter myConverter;
private readonly int uuid = ModBase.GetUuid();
public MyMsgLogin(ModMain.MyMsgBoxConverter converter)
{
try
{
InitializeComponent();
Btn1.Name += ModBase.GetUuid();
Btn2.Name += ModBase.GetUuid();
Btn3.Name += ModBase.GetUuid();
myConverter = converter;
ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d);
data = (JsonObject)converter.Content;
oAuthUrl = converter.AuthUrl?.ToString() ?? "";
Init();
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Account.LoginDialog.Error.Init"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Account.LoginDialog.Error.Init"));
}
Loaded += Load;
}
private void Load(object sender, EventArgs e)
{
try
{
// 动画
Opacity = 0d;
ModAnimation.AniStart(
ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground, BlurBorder.BackgroundProperty,
(myConverter.IsWarn
? new ModBase.MyColor(140d, 80d, 0d, 0d)
: new ModBase.MyColor(90d, 0d, 0d, 0d)) - ModMain.frmMain.PanMsgBackground.Background, 200),
"PanMsgBackground Background");
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(this, 1d, 120, 60),
ModAnimation.AaDouble(i => TransformPos.Y += (double)i,
-TransformPos.Y, 300, 60, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
-TransformRotate.Angle, 300, 60,
new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak))
}, "MyMsgBox " + uuid);
// 记录日志
ModBase.Log($"[Control] 正版验证弹窗:{LabTitle.Text}\r\n{LabCaption.Text}");
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Account.LoginDialog.Error.Load"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Account.LoginDialog.Error.Load"));
}
}
private void Close()
{
// 动画
ModAnimation.AniStart(new[]
{
ModAnimation.AaCode(() =>
{
if (!ModMain.WaitingMyMsgBox.Any())
ModAnimation.AniStart(ModAnimation.AaColor(ModMain.frmMain.PanMsgBackground,
BlurBorder.BackgroundProperty,
new ModBase.MyColor(0d, 0d, 0d, 0d) - ModMain.frmMain.PanMsgBackground.Background, 200,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)));
}, 30),
ModAnimation.AaOpacity(this, -Opacity, 80, 20),
ModAnimation.AaDouble(i => TransformPos.Y += (double)i, 20d - TransformPos.Y,
150, 0, new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaDouble(i => TransformRotate.Angle += (double)i,
6d - TransformRotate.Angle, 150, 0, new ModAnimation.AniEaseInFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaCode(() => ((Grid)Parent).Children.Remove(this), after: true)
}, "MyMsgBox " + uuid);
}
// 实现回车和 Esc 的接口(#4857)
public void Btn1_Click(object sender, MouseButtonEventArgs e)
{
}
public void Btn3_Click(object sender, MouseButtonEventArgs e)
{
Finished(new ThreadInterruptedException());
}
private void Drag(object sender, MouseButtonEventArgs e)
{
// On Error Resume Next
if (e.GetPosition(ShapeLine).Y <= 2d)
ModMain.frmMain.DragMove();
}
#endregion
}
@@ -0,0 +1,31 @@
<Grid x:Class="PCL.MySkin"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL"
Height="64" Width="64" UseLayoutRounding="True" RenderTransformOrigin="0.5,0.5"
Background="{StaticResource ColorBrushSemiTransparent}"
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="-50" ToolTipService.HorizontalOffset="2"
ToolTipService.InitialShowDelay="100">
<Grid.Effect>
<DropShadowEffect x:Name="ShadowSkin" BlurRadius="10" ShadowDepth="0" Color="{DynamicResource ColorObject2}"
Opacity="0.2" />
</Grid.Effect>
<Grid.RenderTransform>
<ScaleTransform />
</Grid.RenderTransform>
<Grid.ContextMenu>
<ContextMenu>
<local:MyMenuItem x:Name="BtnSkinSave" Header="{DynamicResource Launch.Skin.SaveAs}" Padding="0,2"
Checked="BtnSkinSave_Checked"
Icon="F1 M 48,39L 56,39L 56,49L 63.25,49L 52,60.25L 40.75,49L 48,49L 48,39 Z M 20,20L 50.25,20L 56,25.75L 56,38L 52,38L 52,27.25L 48.75,24L 48,24L 48,37L 28,37L 28,24L 24,24L 24,52L 42.25,52L 46.25,56L 20,56L 20,20 Z M 39,24L 39,34L 44,34L 44,24L 39,24 Z" />
<local:MyMenuItem x:Name="BtnSkinRefresh" Header="{DynamicResource Common.Action.Refresh}" Padding="0,2"
Icon="F1 M 38,20.5833C 42.9908,20.5833 47.4912,22.6825 50.6667,26.046L 50.6667,17.4167L 55.4166,22.1667L 55.4167,34.8333L 42.75,34.8333L 38,30.0833L 46.8512,30.0833C 44.6768,27.6539 41.517,26.125 38,26.125C 31.9785,26.125 27.0037,30.6068 26.2296,36.4167L 20.6543,36.4167C 21.4543,27.5397 28.9148,20.5833 38,20.5833 Z M 38,49.875C 44.0215,49.875 48.9963,45.3932 49.7703,39.5833L 55.3457,39.5833C 54.5457,48.4603 47.0852,55.4167 38,55.4167C 33.0092,55.4167 28.5088,53.3175 25.3333,49.954L 25.3333,58.5833L 20.5833,53.8333L 20.5833,41.1667L 33.25,41.1667L 38,45.9167L 29.1487,45.9167C 31.3231,48.3461 34.483,49.875 38,49.875 Z" />
<local:MyMenuItem x:Name="BtnSkinCape" Header="{DynamicResource Launch.Skin.ChangeCape}" Padding="0,2"
Icon="M764.330761 922.841896h-496.758545c-18.741345 0-33.869901-15.128556-33.869901-33.8699V540.789416l-68.642999 29.805512c-15.805954 6.77398-34.0957 0.903197-42.676075-13.999559L22.354135 386.116869c-7.225579-12.418964-5.870783-28.224917 3.38699-39.063286 36.805292-42.901874 256.959647-176.349283 300.764718-202.767806 15.805954-9.483572 36.353693-4.515987 46.063065 11.064168 43.579272 69.320397 86.932745 74.739581 143.156781 74.739581s99.577508-5.419184 143.156781-74.739581c9.709372-15.580154 30.257111-20.54774 46.063065-11.064168 43.805072 26.418523 263.959427 159.865932 300.764719 202.767806 9.483572 11.064168 10.838368 26.644322 3.38699 39.063286l-100.029107 170.4785c-8.580375 14.676957-26.870121 20.773539-42.676075 13.999559l-68.642999-29.805512v348.18258c0.451599 18.741345-14.676957 33.869901-33.418302 33.8699z m-462.888644-67.739801h429.018743v-365.794929c0-11.515766 5.644983-22.128335 15.354355-28.224917 9.483572-6.322381 21.676736-7.225579 32.063506-2.709592l88.287541 38.385888 70.900993-120.802646c-44.25667-34.547299-153.091951-104.093495-239.798898-157.15634-57.578831 72.481588-123.286439 79.029768-181.316869 79.029768s-123.738037-6.548181-181.316868-79.029768c-86.706946 52.837045-195.768026 122.609041-239.798898 157.15634L165.510915 496.758545l88.513341-38.611687c10.38677-4.515987 22.579934-3.612789 32.063506 2.709592 9.483572 6.322381 15.354355 16.93495 15.354355 28.224917v366.020728z" />
</ContextMenu>
</Grid.ContextMenu>
<Image x:Name="ImgBack" Width="48" Height="48" Stretch="Fill" RenderOptions.BitmapScalingMode="NearestNeighbor"
SnapsToDevicePixels="True" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Image x:Name="ImgFore" Width="56" Height="56" Stretch="Fill" RenderOptions.BitmapScalingMode="NearestNeighbor"
SnapsToDevicePixels="True" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
@@ -0,0 +1,498 @@
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using PCL.Core.App.Localization;
using PCL.Core.UI;
using PCL.Network;
namespace PCL;
public partial class MySkin
{
public delegate void ClickEventHandler(object sender, MouseButtonEventArgs e);
// 皮肤储存
private bool isChanging;
// 点击
private bool isSkinMouseDown;
public ModLoader.LoaderTask<ModBase.EqualableList<string>, string> loader;
public MySkin()
{
InitializeComponent();
MouseEnter += PanSkin_MouseEnter;
MouseLeave += PanSkin_MouseLeave;
MouseLeftButtonDown += PanSkin_MouseLeftButtonDown;
MouseLeftButtonUp += PanSkin_MouseLeftButtonUp;
// Handles
BtnSkinSave.Click += BtnSkinSave_Click;
BtnSkinSave.Checked += BtnSkinSave_Checked;
BtnSkinRefresh.Click += RefreshClick;
BtnSkinCape.Click += BtnSkinCape_Click;
}
public string Address
{
get => field;
set
{
field = value;
ToolTip = string.IsNullOrEmpty(field)
? Lang.Text("Common.State.Loading")
: Lang.Text("Launch.Skin.Change.ToolTip");
}
}
// 披风
public bool HasCape
{
get => BtnSkinCape.Visibility == Visibility.Collapsed;
set => BtnSkinCape.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
}
// 事件
public event ClickEventHandler? Click;
// 控件动画
private void PanSkin_MouseEnter(object sender, MouseEventArgs e)
{
ModAnimation.AniStart(ModAnimation.AaOpacity(ShadowSkin, 0.8d - ShadowSkin.Opacity, 200, 100), "Skin Shadow");
}
private void PanSkin_MouseLeave(object sender, MouseEventArgs e)
{
ModAnimation.AniStart(ModAnimation.AaOpacity(ShadowSkin, 0.2d - ShadowSkin.Opacity, 200), "Skin Shadow");
isSkinMouseDown = false;
ModAnimation.AniStart(
ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, 60,
ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale");
}
private void PanSkin_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
isSkinMouseDown = true;
ModAnimation.AniStart(
ModAnimation.AaScaleTransform(this, 0.9d - ((ScaleTransform)RenderTransform).ScaleX, 60,
ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale");
}
private void PanSkin_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
ModAnimation.AniStart(
ModAnimation.AaScaleTransform(this, 1d - ((ScaleTransform)RenderTransform).ScaleX, 60,
ease: new ModAnimation.AniEaseOutFluent()), "Skin Scale");
if (!isSkinMouseDown) return;
isSkinMouseDown = false;
Click?.Invoke(sender, e);
}
// 保存皮肤
public void BtnSkinSave_Click(object sender, RoutedEventArgs e)
{
Save(loader);
}
public static void Save(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> loader)
{
var address = loader.output;
if (loader.State != ModBase.LoadState.Finished)
{
HintService.Hint(Lang.Text("Launch.Skin.Fetching"), HintType.Error);
if (loader.State != ModBase.LoadState.Loading)
loader.Start();
return;
}
try
{
var fileAddress = SystemDialogs.SelectSaveFile(Lang.Text("Launch.Skin.SaveDialog.Title"),
ModBase.GetFileNameFromPath(address),
Lang.Text("Launch.Skin.SaveDialog.Filter"));
if (!fileAddress.Contains(@"\")) return;
File.Delete(fileAddress);
if (address.StartsWith(ModBase.pathImage))
{
var image = new MyBitmap(address);
image.Save(fileAddress);
}
else
{
ModBase.CopyFile(address, fileAddress);
}
HintService.Hint(Lang.Text("Launch.Skin.SaveSuccess"), HintType.Success);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Save.Error"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Skin.Save.Error"));
}
}
private void BtnSkinSave_Checked(object sender, RoutedEventArgs e)
{
((MyMenuItem)sender).IsEnabled = string.IsNullOrEmpty(Address);
}
/// <summary>
/// 载入皮肤。
/// </summary>
public void Load()
{
try
{
// 检查文件存在
Address = loader.output;
if (string.IsNullOrEmpty(Address))
throw new Exception("皮肤加载器 " + loader.name + " 没有输出");
if (!Address.StartsWith(ModBase.pathImage) && !File.Exists(Address))
throw new FileNotFoundException("皮肤文件未找到", Address);
// 加载
MyBitmap image;
try
{
image = new MyBitmap(Address);
}
catch (Exception ex) // #2272
{
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Load.Error.Corrupted", Address),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Skin.Load.Error.Corrupted", Address));
File.Delete(Address);
return;
}
ImgBack.Tag = Address;
// 大小检查
var scale = (int)Math.Round(image.pic.Width / 64d);
if (image.pic.Width < 32 || image.pic.Height < 32)
{
ImgFore.Source = null;
ImgBack.Source = null;
throw new Exception("图片大小不足,长为 " + image.pic.Height + ",宽为 " + image.pic.Width);
}
MyBitmap skinHead = null;
// 头发层(附加层)
if (image.pic.Width >= 64 && image.pic.Height >= 32)
{
if (image.pic.GetPixel(1, 1).A == 0 ||
image.pic.GetPixel(image.pic.Width - 1, image.pic.Height - 1).A == 0 ||
image.pic.GetPixel(image.pic.Width - 2, (int)Math.Round(image.pic.Height / 2d - 2d)).A == 0 ||
(image.pic.GetPixel(1, 1) != image.pic.GetPixel(scale * 41, scale * 9) &&
image.pic.GetPixel(image.pic.Width - 1, image.pic.Height - 1) !=
image.pic.GetPixel(scale * 41, scale * 9) &&
image.pic.GetPixel(image.pic.Width - 2, (int)Math.Round(image.pic.Height / 2d - 2d)) !=
image.pic.GetPixel(scale * 41, scale * 9))) // 如果图片中有任何透明像素(避免纯色白底)
// 或是头部颜色和透明区均不一样
{
ImgFore.Source = image.Clip(scale * 40, scale * 8, scale * 8, scale * 8);
skinHead = image.Clip(scale * 40, scale * 8, scale * 8, scale * 8);
}
else
{
ImgFore.Source = null;
}
}
else
{
ImgFore.Source = null;
}
// 脸层
ImgBack.Source = image.Clip(scale * 8, scale * 8, scale * 8, scale * 8);
// 用于显示档案列表头像的图片
var skinHeadId = Address.Between(new[] { Address.Contains("Images/Skins/") ? "Skins/" : @"Skin\" }[0],
".png");
var cachePath = ModBase.pathTemp + $@"Cache\Skin\Head\{skinHeadId}.png";
ModProfile.selectedProfile.SkinHeadId = skinHeadId;
ModProfile.SaveProfile();
var completeHead = new Bitmap(56, 56);
using (var g = Graphics.FromImage(completeHead))
{
g.InterpolationMode = InterpolationMode.NearestNeighbor;
g.PixelOffsetMode = PixelOffsetMode.Half;
using (Bitmap faceBitmap = image.Clip(scale * 8, scale * 8, scale * 8, scale * 8))
{
g.DrawImage(faceBitmap, new Rectangle(4, 4, 48, 48));
}
if (ImgFore.Source is not null)
{
using Bitmap hairBitmap = image.Clip(scale * 40, scale * 8, scale * 8, scale * 8);
g.DrawImage(hairBitmap, new Rectangle(0, 0, 56, 56));
}
}
if (!Directory.Exists(ModBase.pathTemp + @"Cache\Skin\Head"))
Directory.CreateDirectory(ModBase.pathTemp + @"Cache\Skin\Head");
completeHead.Save(cachePath, ImageFormat.Png);
ModBase.Log("[Skin] 载入头像成功:" + loader.name);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Load.Error.Avatar", $"{(Address ?? "null")},{loader.name}"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Skin.Load.Error.Avatar", $"{(Address ?? "null")},{loader.name}"));
}
}
private object ScaleToSize(Bitmap bitmap, int width, int height)
{
var scaledBitmap = new Bitmap(width, height);
using var g = Graphics.FromImage(scaledBitmap);
g.InterpolationMode = InterpolationMode.NearestNeighbor;
g.PixelOffsetMode = PixelOffsetMode.Half;
g.DrawImage(bitmap, 0, 0, width, height);
return scaledBitmap;
}
/// <summary>
/// 清空皮肤。
/// </summary>
public void Clear()
{
Address = "";
ImgFore.Source = null;
ImgBack.Source = null;
}
// 刷新缓存
public void RefreshClick(object sender, RoutedEventArgs e)
{
RefreshCache(loader);
}
/// <summary>
/// 刷新皮肤缓存。
/// </summary>
public static void RefreshCache(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> sender = null)
{
var hasLoaderRunning =
PageLaunchLeft.skinLoaders.Any(skinLoader => skinLoader.State == ModBase.LoadState.Loading);
if (ModMain.frmLaunchLeft is not null && hasLoaderRunning)
// 由于 Abort 不是实时的,暂时不会释放文件,会导致删除报错,故只能取消执行
HintService.Hint(Lang.Text("Launch.Skin.Refresh.Busy"));
else
// 清空缓存
// 刷新控件
ModBase.RunInThread(() =>
{
try
{
HintService.Hint(Lang.Text("Launch.Skin.Refreshing"));
ModBase.Log("[Skin] 正在清空皮肤缓存");
if (Directory.Exists(ModBase.pathTemp + @"Cache\Skin"))
ModBase.DeleteDirectory(ModBase.pathTemp + @"Cache\Skin");
if (Directory.Exists(ModBase.pathTemp + @"Cache\Uuid"))
ModBase.DeleteDirectory(ModBase.pathTemp + @"Cache\Uuid");
ModBase.IniClearCache(ModBase.pathTemp + @"Cache\Skin\IndexMs.ini");
ModBase.IniClearCache(ModBase.pathTemp + @"Cache\Skin\IndexAuth.ini");
ModBase.IniClearCache(ModBase.pathTemp + @"Cache\Uuid\Mojang.ini");
foreach (var SkinLoader in sender is not null
? new[] { sender }
: new[] { PageLaunchLeft.skinLegacy, PageLaunchLeft.skinMs })
SkinLoader.WaitForExit(isForceRestart: true);
HintService.Hint(Lang.Text("Launch.Skin.RefreshSuccess"), HintType.Success);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Refresh.Error"),
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Launch.Skin.Refresh.Error"));
}
});
}
/// <summary>
/// 在更换正版皮肤后,刷新正版皮肤。
/// </summary>
/// <param name="skinAddress">新的正版皮肤完整地址。</param>
public static void ReloadCache(string skinAddress)
{
// 更新缓存
// 刷新控件
// 完成提示
ModBase.RunInThread(() =>
{
try
{
ModBase.WriteIni(ModBase.pathTemp + @"Cache\Skin\IndexMs.ini", ModProfile.selectedProfile.Uuid,
skinAddress);
ModBase.Log($"[Skin] 已写入皮肤地址缓存 {ModProfile.selectedProfile.Uuid} -> {skinAddress}");
PageLaunchLeft.skinMs.WaitForExit(isForceRestart: true);
HintService.Hint(Lang.Text("Launch.Skin.ChangeSuccess"), HintType.Success);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Change.Error.MsRefresh"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Launch.Skin.Change.Error.MsRefresh"));
}
});
}
public void BtnSkinCape_Click(object sender, RoutedEventArgs e)
{
// 检查条件,获取新披风
if (isChanging)
{
HintService.Hint(Lang.Text("Launch.Skin.Cape.Changing"));
return;
}
if (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Failed)
{
HintService.Hint(Lang.Text("Launch.Skin.Cape.LoginFailed"), HintType.Error);
return;
}
HintService.Hint(Lang.Text("Launch.Skin.Cape.FetchingList"));
isChanging = true;
// 开始实际获取
ModBase.RunInNewThread(() =>
{
try
{
// 获取登录信息
if (ModLaunch.mcLoginMsLoader.State != ModBase.LoadState.Finished)
ModLaunch.mcLoginMsLoader.WaitForExit(ModProfile.GetLoginData());
if (ModLaunch.mcLoginMsLoader.State != ModBase.LoadState.Finished)
{
HintService.Hint(Lang.Text("Launch.Skin.Cape.LoginFailed"), HintType.Error);
return;
}
var accessToken = ModLaunch.mcLoginMsLoader.output.AccessToken;
var uuid = ModLaunch.mcLoginMsLoader.output.Uuid;
var skinData = (JsonObject)ModBase.GetJson(ModLaunch.mcLoginMsLoader.output.ProfileJson);
foreach (var itemSkin in skinData["capes"].AsArray())
{
if (itemSkin["url"] is null)
continue;
var localFile = $@"{ModBase.pathTemp}Cache\Capes\{itemSkin["alias"]}.png";
var capeFrontFile = $@"{ModBase.pathTemp}Cache\Capes\{itemSkin["alias"]}-front.png";
if (File.Exists(localFile) && File.Exists(capeFrontFile))
{
itemSkin["url"] = capeFrontFile;
continue;
}
FileDownloader.DownloadByLoader(itemSkin["url"].ToString(), localFile);
var capeFrontRegion = new Rectangle(1, 0, 11, 17);
var capeFront = new Bitmap(capeFrontRegion.Width, capeFrontRegion.Height);
var capeImage = Image.FromFile(localFile);
var gra = Graphics.FromImage(capeFront);
gra.DrawImage(capeImage, capeFrontRegion, capeFrontRegion, GraphicsUnit.Pixel);
capeFront.Save(capeFrontFile);
itemSkin["url"] = capeFrontFile;
}
// 获取玩家的所有披风
int? selId = null;
ModBase.RunInUiWait(() =>
{
try
{
var selectionControl = new List<IMyRadio>
{
new MyListItem
{
Title = Lang.Text("Launch.Skin.Cape.None"),
Info = "Null"
}
};
selectionControl.AddRange(from Cape in skinData["capes"].AsArray()
let CapeAlias = Cape["alias"].ToString()
let CapeName = _GetCapeDisplayName(CapeAlias)
let state = Cape["state"]
let active = state is not null && state.ToString().ToUpper().Equals("ACTIVE")
select new MyListItem
{
Title = CapeName,
Info = Cape["alias"].ToString(),
Checked = active,
Type = MyListItem.CheckType.RadioBox,
Logo = (string)Cape["url"],
LogoScale = 0.8d
});
selId = ModMain.MyMsgBoxSelect(selectionControl, Lang.Text("Launch.Skin.Cape.SelectTitle"),
Lang.Text("Common.Action.Confirm"), Lang.Text("Common.Action.Cancel"));
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Cape.Error.List"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Launch.Skin.Cape.Error.List"));
}
});
if (selId is null)
return;
// 发送请求
var result = Requester.Fetch("https://api.minecraftservices.com/minecraft/profile/capes/active",
new FetchParam
{
Method = selId is 0 ? "DELETE" : "PUT",
Content = selId is 0
? ""
: new JsonObject { ["capeId"] = skinData["capes"][(int)(selId - 1)]["id"]?.ToString() }.ToJsonString(),
ContentType = "application/json",
Headers = new Dictionary<string, string> { { "Authorization", "Bearer " + accessToken } }
}
);
if (result.Contains("\"errorMessage\""))
HintService.Hint(
Lang.Text("Launch.Skin.Cape.ChangeFailedWithReason",
((JsonObject)ModBase.GetJson(result))["errorMessage"]), HintType.Error);
else
HintService.Hint(Lang.Text("Launch.Skin.Cape.ChangeSuccess"), HintType.Success);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Cape.ChangeFailed"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Skin.Cape.ChangeFailed"));
}
finally
{
isChanging = false;
}
}, "Cape Change");
}
private static string _GetCapeDisplayName(string capeAlias)
{
var safeName = capeAlias
.Replace("-", "")
.Replace(" ", "")
.Replace("'", "");
var key = $"Launch.Skin.Cape.Name.{safeName}";
var name = Lang.Text(key);
if (name == $"!{key}!" || name == key)
return capeAlias;
return name;
}
}
@@ -0,0 +1,157 @@
<local:MyPageLeft x:Class="PCL.PageLaunchLeft"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PCL"
mc:Ignorable="d" x:Name="PanBack"
d:DesignHeight="417.2" Width="300">
<Grid Name="PanInput" RenderTransformOrigin="0.5,0.5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
</Grid.ColumnDefinitions>
<Grid.RenderTransform>
<ScaleTransform />
</Grid.RenderTransform>
<local:MyButton Grid.Row="3" Grid.Column="2" x:Name="BtnInstance" Height="35" Margin="0,10,10,0" Text="{DynamicResource Launch.Home.SelectInstance}"
IsEnabled="False" Click="BtnInstance_Click" />
<local:MyButton Grid.Row="3" Grid.Column="3" x:Name="BtnMore" Visibility="Collapsed" Height="35"
Click="BtnMore_Click"
Margin="0,10,10,0" TextPadding="36" Text="{DynamicResource Launch.Home.InstanceSettings}" />
<Grid Grid.ColumnSpan="5" Grid.Row="1" Margin="20,0" VerticalAlignment="Center" x:Name="PanLogin" />
<Grid Grid.ColumnSpan="5" Grid.Row="2" RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="AprilScaleTrans" />
<TranslateTransform x:Name="AprilPosTrans" />
</TransformGroup>
</Grid.RenderTransform>
<local:MyButton x:Name="BtnLaunch" Height="54" Margin="20,0" Text="{DynamicResource Launch.Home.Button.Loading}" ColorType="Highlight"
Click="BtnLaunch_Click"
Padding="30,0,30,15" IsEnabled="False" />
<TextBlock x:Name="LabVersion" Text="{DynamicResource Launch.Home.VersionList.Loading}" Margin="35,0,35,10" IsHitTestVisible="False"
VerticalAlignment="Bottom" HorizontalAlignment="Center" TextTrimming="CharacterEllipsis"
FontSize="11" Foreground="{DynamicResource ColorBrushGray3}" RenderTransformOrigin="0.5,-0.2">
<TextBlock.RenderTransform>
<ScaleTransform ScaleX="{Binding RealRenderTransform.ScaleX, ElementName=BtnLaunch, Mode=OneWay}"
ScaleY="{Binding RealRenderTransform.ScaleY, ElementName=BtnLaunch, Mode=OneWay}" />
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</Grid>
<Grid Name="PanLaunching" RenderTransformOrigin="0.5,0.5" Visibility="Collapsed" Opacity="0">
<Grid.RenderTransform>
<ScaleTransform ScaleX="0.8" ScaleY="0.8" />
</Grid.RenderTransform>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="1" RenderTransformOrigin="0.5,0.5" Margin="0,-7,0,0">
<StackPanel.BitmapEffect>
<DropShadowBitmapEffect Color="{StaticResource ColorObjectGray2}" ShadowDepth="1.5" Direction="270"
Opacity="0.15" Softness="0.15" />
</StackPanel.BitmapEffect>
<local:MyLoading x:Name="LoadLaunching" AutoRun="False" Height="50" Margin="0,10,0,5" />
<TextBlock Name="LabLaunchingTitle" Margin="15,10,15,0" Text="{DynamicResource Launch.Status.Title.Launching}" HorizontalAlignment="Center"
FontSize="20" Foreground="{DynamicResource ColorBrush3}">
<TextBlock.RenderTransform>
<SkewTransform AngleX="-3" />
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock Name="LabLaunchingName" Margin="40,5,40,0" FontSize="13.5" Text="Forge 1.12.2-15.8.0.1560"
HorizontalAlignment="Center" Foreground="{DynamicResource ColorBrush3}"
RenderTransformOrigin="0.5,0.5">
<TextBlock.RenderTransform>
<SkewTransform AngleX="-3" />
</TextBlock.RenderTransform>
</TextBlock>
<Grid Height="4" Margin="30,12,30,27" SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Name="ProgressLaunchingFinished" Width="69.28*" />
<ColumnDefinition Name="ProgressLaunchingUnfinished" Width="30.72*" />
</Grid.ColumnDefinitions>
<Rectangle Grid.Column="0">
<Rectangle.Fill>
<LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
<GradientStop Color="{DynamicResource ColorObject4}" Offset="0" />
<GradientStop Color="{DynamicResource ColorObject3}" Offset="0.6" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Column="1" Fill="{DynamicResource ColorBrush6}" Opacity="0.6" />
</Grid>
<Grid HorizontalAlignment="Center" Name="PanLaunchingInfo" SizeChanged="PanLaunchingInfo_SizeChangedW">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="15" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.CurrentStep}" Grid.Row="0" Grid.Column="1"
HorizontalAlignment="Right" Opacity="0.5" />
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.DownloadLibs}" Grid.Row="0" Grid.Column="3"
HorizontalAlignment="Left" Name="LabLaunchingStage" />
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.LoginMethod}" Grid.Row="1" Grid.Column="1"
HorizontalAlignment="Right" Opacity="0.5" />
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.MicrosoftLogin}" Grid.Row="1" Grid.Column="3"
HorizontalAlignment="Left" MaxWidth="160" TextWrapping="WrapWithOverflow"
Name="LabLaunchingMethod" />
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.LaunchProgress}" Grid.Row="2" Grid.Column="1"
HorizontalAlignment="Right" Opacity="0.5" Name="LabLaunchingProgressLeft" />
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="69.28 %" Grid.Row="2" Grid.Column="3"
HorizontalAlignment="Left" Name="LabLaunchingProgress" />
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="{DynamicResource Launch.Status.DownloadSpeed}" Grid.Row="3" Grid.Column="1"
HorizontalAlignment="Right" Opacity="0" Name="LabLaunchingDownloadLeft"
Visibility="Collapsed" />
<TextBlock FontSize="12.5" Margin="0,0,0,5" Text="5.2 M/s" Grid.Row="3" Grid.Column="3"
HorizontalAlignment="Left" Name="LabLaunchingDownload" Opacity="0" Visibility="Collapsed" />
<Grid x:Name="PanLaunchingHint" HorizontalAlignment="Center" Grid.Row="2" Grid.ColumnSpan="5"
Grid.RowSpan="2" Width="260" Visibility="Collapsed" Margin="0,16,0,2" Opacity="0">
<Border Margin="0,8,0,0" BorderThickness="1" CornerRadius="3"
BorderBrush="{DynamicResource ColorBrushGray1}" Opacity="0.5">
<Border.Clip>
<CombinedGeometry GeometryCombineMode="Exclude">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="0,0,1000,1000" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<RectangleGeometry Rect="94,0,72,10" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Border.Clip>
</Border>
<TextBlock FontSize="12.5" Text="{DynamicResource Launch.Status.Trivia}" HorizontalAlignment="Center" VerticalAlignment="Top"
Opacity="0.5" />
<TextBlock x:Name="LabLaunchingHint" FontSize="12.5" Text="{DynamicResource Launch.Status.Trivia.Placeholder}"
Margin="11,21,11,10" TextWrapping="Wrap" HorizontalAlignment="Center" MaxHeight="54"
TextTrimming="CharacterEllipsis" />
</Grid>
</Grid>
</StackPanel>
<local:MyButton Grid.Row="4" x:Name="BtnCancel" Height="35" Margin="20,0,20,20" VerticalAlignment="Bottom"
Click="BtnCancel_Click"
Text="{DynamicResource Common.Action.Cancel}" />
</Grid>
</local:MyPageLeft>
@@ -0,0 +1,1033 @@
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.Utils;
using PCL.Network;
namespace PCL;
public partial class PageLaunchLeft
{
private double actualUsedHeight;
private double actualUsedWidth;
private int btnLaunchState;
private string _btnLaunchLanguage;
private McInstance btnLaunchVersion;
private bool isHeightAnimating;
public interface ILoginPage { void Reload(); }
private enum LaunchButtonAction
{
Loading,
Launch,
Download,
Disabled
}
private LaunchButtonAction _launchButtonAction;
private static string StageWaitWindow => Lang.Text("Minecraft.Launch.Stage.WaitWindow");
private static string StageEnd => Lang.Text("Minecraft.Launch.Stage.End");
private static string StageRoot => Lang.Text("Minecraft.Launch.Stage.Root");
// 加载当前实例
private bool isLoad;
private bool isLoadFinished;
// 尺寸改变动画
private bool isWidthAnimating;
private double showProgress;
public PageLaunchLeft()
{
InitializeComponent();
Loaded += PageLaunchLeft_Loaded;
WeakLanguageChanged.Add(this, OnLanguageChanged);
// Handles
BtnInstance.Click += BtnInstance_Click;
BtnLaunch.Click += BtnLaunch_Click;
BtnLaunch.Loaded += (_, _) => RefreshButtonsUI();
BtnCancel.Click += BtnCancel_Click;
BtnMore.Click += BtnMore_Click;
PanLaunchingInfo.SizeChanged += PanLaunchingInfo_SizeChangedW;
PanLaunchingInfo.SizeChanged += PanLaunchingInfo_SizeChangedH;
}
private static void OnLanguageChanged(PageLaunchLeft page) => ModBase.RunInUi(page.RefreshButtonsUI);
public void PageLaunchLeft_Loaded(object sender, RoutedEventArgs e)
{
if (isLoad)
RefreshPage(false);
AprilPosTrans.X = 0d;
AprilPosTrans.Y = 0d;
if (isLoad)
return;
isLoad = true;
ModAnimation.AniControlEnabled += 1;
// 开始按钮
ModInstanceList.mcInstanceListLoader.LoadingStateChanged += (_, _) => RefreshButtonsUI();
ModFolder.mcFolderListLoader.LoadingStateChanged += (_, _) => RefreshButtonsUI();
RefreshButtonsUI();
// 初始化档案
ModProfile.GetProfile();
if (!(ModProfile.profileList.Count == 0) && ModProfile.lastUsedProfile >= 0 &&
ModProfile.lastUsedProfile < ModProfile.profileList.Count)
ModProfile.selectedProfile = ModProfile.profileList[ModProfile.lastUsedProfile];
// 加载实例
ModBase.RunInNewThread(() =>
{
// 自动整合包安装:准备
string packInstallPath = null;
if (File.Exists(Path.Combine(ModBase.exePath, "modpack.zip")))
packInstallPath = Path.Combine(ModBase.exePath, "modpack.zip");
if (File.Exists(Path.Combine(ModBase.exePath, "modpack.mrpack")))
packInstallPath = Path.Combine(ModBase.exePath, "modpack.mrpack");
if (packInstallPath is not null)
{
ModBase.Log("[Launch] 需自动安装整合包:" + packInstallPath, ModBase.LogLevel.Debug);
States.Game.SelectedFolder = @"$.minecraft\";
if (!Directory.Exists(ModBase.exePath + @".minecraft\"))
{
Directory.CreateDirectory(ModBase.exePath + @".minecraft\");
Directory.CreateDirectory(ModBase.exePath + @".minecraft\versions\");
ModFolder.McFolderLauncherProfilesJsonCreate(ModBase.exePath + @".minecraft\");
}
PageSelectLeft.AddFolder(ModBase.exePath + @".minecraft\",
ModBase.GetFolderNameFromPath(ModBase.exePath), false);
ModFolder.mcFolderListLoader.WaitForExit();
}
// 确认 Minecraft 文件夹存在
ModFolder.mcFolderSelected =
States.Game.SelectedFolder.ToString().Replace("$", ModBase.exePath);
if (string.IsNullOrEmpty(ModFolder.mcFolderSelected) || !Directory.Exists(ModFolder.mcFolderSelected))
{
// 无效的文件夹
if (string.IsNullOrEmpty(ModFolder.mcFolderSelected))
ModBase.Log("[Launch] 没有已储存的 Minecraft 文件夹");
else
ModBase.Log("[Launch] Minecraft 文件夹无效,该文件夹已不存在:" + ModFolder.mcFolderSelected,
ModBase.LogLevel.Debug);
ModFolder.mcFolderListLoader.WaitForExit(isForceRestart: true);
States.Game.SelectedFolder = ModFolder.mcFolderList[0].Location.Replace(ModBase.exePath, "$");
}
ModBase.Log("[Launch] Minecraft 文件夹:" + ModFolder.mcFolderSelected);
if (Config.Debug.AddRandomDelay)
Thread.Sleep(RandomUtils.NextInt(500, 3000));
// 自动整合包安装
if (packInstallPath is not null)
try
{
var installLoader = ModModpack.ModpackInstall(packInstallPath);
ModBase.Log("[Launch] 自动安装整合包已开始:" + packInstallPath);
installLoader.WaitForExit();
if (installLoader.State == ModBase.LoadState.Finished)
{
ModBase.Log("[Launch] 自动安装整合包成功,清理安装包:" + packInstallPath);
if (File.Exists(packInstallPath))
File.Delete(packInstallPath);
}
}
catch (ModBase.CancelledException ex)
{
ModBase.Log(ex, "自动安装整合包被用户取消:" + packInstallPath);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Select.Folder.Error.InstallPack", packInstallPath),
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Select.Folder.Error.InstallPack", packInstallPath));
}
// 确认 Minecraft 版本实例
var selection = States.Game.SelectedInstance;
var instance = selection == "" ? null : new McInstance(selection);
if (instance is null || !instance.PathInstance.StartsWithF(ModFolder.mcFolderSelected) ||
!instance.Check())
{
// 无效的实例
ModBase.Log("[Launch] 当前选择的 Minecraft 实例无效:" + (instance is null ? "null" : instance.PathInstance),
instance is null ? ModBase.LogLevel.Normal : ModBase.LogLevel.Debug);
if (ModInstanceList.mcInstanceListLoader.State != ModBase.LoadState.Finished)
ModLoader.LoaderFolderRun(ModInstanceList.mcInstanceListLoader, ModFolder.mcFolderSelected,
ModLoader.LoaderFolderRunType.ForceRun, 1, @"versions\", true);
if (ModInstanceList.mcInstanceList.Count == 0 ||
ModInstanceList.mcInstanceList.First().Value[0].Logo.Contains("RedstoneBlock"))
{
instance = null;
States.Game.SelectedInstance = "";
ModBase.Log("[Launch] 无可用 Minecraft 实例");
}
else
{
instance = ModInstanceList.mcInstanceList.First().Value[0];
States.Game.SelectedInstance = instance.Name;
ModBase.Log("[Launch] 自动选择 Minecraft 实例:" + instance.PathInstance);
}
}
ModBase.RunInUi(() =>
{
ModInstanceList.McMcInstanceSelected = instance; // 绕这一圈是为了避免 McInstanceCheck 触发第二次实例改变
isLoadFinished = true;
RefreshButtonsUI();
RefreshPage(false); // 有可能选择的版本变化了,需要重新刷新
// If IsProfileVaild() = "" Then McLoginLoader.Start() '自动登录
});
}, "Instance Check", ThreadPriority.AboveNormal);
// 改变页面
RefreshPage(false);
ModAnimation.AniControlEnabled -= 1;
}
// 实例选择按钮
private void BtnInstance_Click(object sender, MouseButtonEventArgs e)
{
if (ModLaunch.mcLaunchLoader.State == ModBase.LoadState.Loading)
return;
ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect);
}
// 启动按钮
public void LaunchButtonClick()
{
if (ModLaunch.mcLaunchLoader.State == ModBase.LoadState.Loading || !BtnLaunch.IsEnabled ||
(ModMain.frmMain.pageRight is not null &&
ModMain.frmMain.pageRight.PageState != MyPageRight.PageStates.ContentStay &&
ModMain.frmMain.pageRight.PageState != MyPageRight.PageStates.ContentEnter))
return;
// 愚人节处理
if (ModMain.isAprilEnabled && !ModMain.isAprilGiveup)
{
ModMain.isAprilGiveup = true;
ModMain.frmLaunchLeft.AprilScaleTrans.ScaleX = 1d;
ModMain.frmLaunchLeft.AprilScaleTrans.ScaleY = 1d;
ModMain.frmLaunchLeft.AprilPosTrans.X = 0d;
ModMain.frmLaunchLeft.AprilPosTrans.Y = 0d;
ModMain.frmMain.BtnExtraApril.ShowRefresh();
}
// 实际的启动
switch (_launchButtonAction)
{
case LaunchButtonAction.Launch:
{
if (File.Exists(ModInstanceList.McMcInstanceSelected.PathInstance + ".pclignore"))
{
HintService.Hint(Lang.Text("Launch.Home.Instance.InstallingCannotLaunch"), HintType.Error);
return;
}
ModLaunch.McLaunchStart();
break;
}
case LaunchButtonAction.Download:
{
ModMain.frmMain.PageChange(FormMain.PageType.Download, FormMain.PageSubType.DownloadInstall);
break;
}
}
}
public void RefreshButtonsUI()
{
if (!BtnLaunch.IsLoaded)
return;
// 获取当前状态
int currentState;
if (!isLoadFinished || ModInstanceList.mcInstanceListLoader.State == ModBase.LoadState.Loading ||
ModFolder.mcFolderListLoader.State == ModBase.LoadState.Loading)
{
currentState = 0;
}
else if (ModInstanceList.McMcInstanceSelected is null)
{
if (Config.Preference.Hide.PageDownload && !PageSetupUI.HiddenForceShow)
currentState = 1;
else
currentState = 2;
}
else
{
currentState = 3;
}
// 更新状态。
var currentLanguage = LocalizationService.CurrentLanguage.Code;
if (currentState == btnLaunchState &&
currentLanguage == _btnLaunchLanguage &&
((ModInstanceList.McMcInstanceSelected is null ? "" : ModInstanceList.McMcInstanceSelected.PathInstance) ?? "") ==
((btnLaunchVersion is null ? "" : btnLaunchVersion.PathInstance) ?? ""))
goto ExitRefresh;
_btnLaunchLanguage = currentLanguage;
btnLaunchVersion = ModInstanceList.McMcInstanceSelected;
btnLaunchState = currentState;
switch (currentState)
{
case 0:
{
_launchButtonAction = LaunchButtonAction.Loading;
ModBase.Log("[Minecraft] 启动按钮:正在加载 Minecraft 实例");
ModMain.frmLaunchLeft.BtnLaunch.Text = Lang.Text("Launch.Home.Button.Loading");
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = false;
ModMain.frmLaunchLeft.LabVersion.Text = Lang.Text("Launch.Home.Instance.Loading");
ModMain.frmLaunchLeft.BtnInstance.IsEnabled = false;
ModMain.frmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed;
break;
}
case 1:
{
_launchButtonAction = LaunchButtonAction.Disabled;
ModBase.Log("[Minecraft] 启动按钮:无 Minecraft 实例,下载已禁用");
ModMain.frmLaunchLeft.BtnLaunch.Text = Lang.Text("Launch.Home.Button.Launch");
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = false;
ModMain.frmLaunchLeft.LabVersion.Text = Lang.Text("Launch.Home.Instance.NotFound");
ModMain.frmLaunchLeft.BtnInstance.IsEnabled = true;
ModMain.frmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed;
break;
}
case 2:
{
_launchButtonAction = LaunchButtonAction.Download;
ModBase.Log("[Minecraft] 启动按钮:无 Minecraft 实例,要求下载");
ModMain.frmLaunchLeft.BtnLaunch.Text = Lang.Text("Launch.Home.Button.Download");
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = true;
ModMain.frmLaunchLeft.LabVersion.Text = Lang.Text("Launch.Home.Instance.NotFound");
ModMain.frmLaunchLeft.BtnInstance.IsEnabled = true;
ModMain.frmLaunchLeft.BtnMore.Visibility = Visibility.Collapsed;
break;
}
case 3:
{
_launchButtonAction = LaunchButtonAction.Launch;
ModBase.Log("[Minecraft] 启动按钮:Minecraft 实例:" + ModInstanceList.McMcInstanceSelected.PathInstance);
ModMain.frmLaunchLeft.BtnLaunch.Text = Lang.Text("Launch.Home.Button.Launch");
ModMain.frmLaunchLeft.BtnInstance.IsEnabled = true;
if (ModProfile.selectedProfile is not null)
BtnLaunch.IsEnabled = true;
else
BtnLaunch.IsEnabled = false;
ModMain.frmLaunchLeft.LabVersion.Text = ModInstanceList.McMcInstanceSelected.Name;
break;
}
// FrmLaunchLeft.BtnMore.Visibility = Visibility.Visible '由功能隐藏设置修改
}
ExitRefresh: ;
// 功能隐藏
ModMain.frmLaunchLeft.BtnInstance.Visibility =
!PageSetupUI.HiddenForceShow && Config.Preference.Hide.FunctionSelect
? Visibility.Collapsed
: Visibility.Visible;
if (currentState == 3) ModMain.frmLaunchLeft.BtnMore.Visibility = ModMain.frmLaunchLeft.BtnInstance.Visibility;
}
// 取消按钮
private void BtnCancel_Click(object sender, MouseButtonEventArgs e)
{
if (ModLaunch.mcLaunchLoaderReal is not null)
{
ModLaunch.mcLaunchLoaderReal.Abort();
ModLaunch.McLaunchLog("已取消启动");
try
{
if (ModLaunch.mcLaunchWatcher is not null)
ModLaunch.mcLaunchWatcher.Kill();
else if (ModLaunch.mcLaunchProcess is not null)
if (!ModLaunch.mcLaunchProcess.HasExited)
ModLaunch.mcLaunchProcess.Kill();
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Minecraft.Launch.Error.CancelProcess"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Minecraft.Launch.Error.CancelProcess"));
}
}
}
// 实例设置按钮
private void BtnMore_Click(object sender, MouseButtonEventArgs e)
{
if (ModLaunch.mcLaunchLoader.State == ModBase.LoadState.Loading)
return;
ModInstanceList.McMcInstanceSelected.Load();
PageInstanceLeft.McInstance = ModInstanceList.McMcInstanceSelected;
if (File.Exists(ModInstanceList.McMcInstanceSelected.PathInstance + ".pclignore"))
{
HintService.Hint(Lang.Text("Launch.Home.Instance.InstallingCannotSetup"), HintType.Error);
return;
}
ModMain.frmMain.PageChange(FormMain.PageType.InstanceSetup);
}
/// <summary>
/// 每 0.2s 执行一次,刷新启动的数据 UI 显示。
/// </summary>
public void LaunchingRefresh()
{
try
{
if (ModLaunch.mcLaunchLoaderReal.State == ModBase.LoadState.Aborted)
return;
// 阶段状态获取
var isLaunched = false; // 是否已经启动游戏,只是在等待窗口
do
{
try
{
var exitTry = false;
foreach (var Loader in ModLaunch.mcLaunchLoaderReal.GetLoaderList(false))
if (Loader.State == ModBase.LoadState.Loading || Loader.State == ModBase.LoadState.Waiting)
{
LabLaunchingStage.Text = Loader.name;
isLaunched = Loader.name == StageWaitWindow || Loader.name == StageEnd;
exitTry = true;
break;
}
if (exitTry) break;
LabLaunchingStage.Text = Lang.Text("Launch.Status.Completed");
}
catch (Exception ex)
{
ModBase.Log(ex, "获取是否启动完成失败,可能是由于启动状态改变导致集合已修改");
return;
}
} while (false);
if (ModAnimation.AniIsRun("Launch State Page"))
isLaunched = false; // 等待页面切换动画完成
// 计算应显示的进度
var actualProgress = ModLaunch.mcLaunchLoaderReal.Progress;
if (actualProgress >= showProgress)
showProgress += (actualProgress - showProgress) * 0.2d + 0.005d; // 向实际进度靠一点
if (actualProgress <= showProgress)
showProgress = actualProgress; // 原来或处理后变得比实际进度高,直接回退
if (isLaunched)
showProgress = 1d; // 如果已经完成了,就不卖关子了
// 文本
LabLaunchingTitle.Text = isLaunched ? Lang.Text("Launch.Status.Title.Launched") :
ModLaunch.currentLaunchOptions.SaveBatch is null ? Lang.Text("Launch.Status.Title.Launching") : Lang.Text("Launch.Status.Title.ExportingScript");
LabLaunchingProgress.Text = Lang.Number(showProgress, "P2");
var hasLaunchDownloader = false;
try
{
foreach (var Loader in ModNet.NetManager.Tasks)
if (Loader.RealParent is not null && Loader.RealParent.name == StageRoot &&
Loader.State == ModBase.LoadState.Loading)
hasLaunchDownloader = true;
}
catch (Exception ex)
{
ModBase.Log(ex, "获取 Minecraft 启动下载器失败,可能是因为启动被取消");
hasLaunchDownloader = false;
}
LabLaunchingDownload.Text = ModBase.GetString(ModNet.NetManager.Speed) + "/s";
var shouldShowHint = Config.Preference.ShowLaunchingHint;
// 进度改变动画
var animList = new List<ModAnimation.AniData>
{
ModAnimation.AaGridLengthWidth(ProgressLaunchingFinished,
showProgress - ProgressLaunchingFinished.Width.Value, 260,
ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaGridLengthWidth(ProgressLaunchingUnfinished,
1d - showProgress - ProgressLaunchingUnfinished.Width.Value, 260,
ease: new ModAnimation.AniEaseOutFluent())
};
var isDownloadStateChanged =
hasLaunchDownloader == (LabLaunchingDownload.Visibility == Visibility.Collapsed);
if (isDownloadStateChanged)
{
LabLaunchingDownload.Visibility = Visibility.Visible;
LabLaunchingDownloadLeft.Visibility = Visibility.Visible;
animList.AddRange(new[]
{
ModAnimation.AaOpacity(LabLaunchingDownload,
(hasLaunchDownloader ? 1 : 0) - LabLaunchingDownload.Opacity, 100),
ModAnimation.AaOpacity(LabLaunchingDownloadLeft,
(hasLaunchDownloader ? 0.5d : 0d) - LabLaunchingDownloadLeft.Opacity, 100),
ModAnimation.AaCode(() =>
{
if (!hasLaunchDownloader)
{
LabLaunchingDownload.Visibility = Visibility.Collapsed;
LabLaunchingDownloadLeft.Visibility = Visibility.Collapsed;
}
}, 110)
});
}
var isProgressStateChanged = !isLaunched == (LabLaunchingProgress.Visibility == Visibility.Collapsed);
if (isProgressStateChanged)
{
LabLaunchingProgress.Visibility = Visibility.Visible;
LabLaunchingProgressLeft.Visibility = Visibility.Visible;
if (isLaunched && shouldShowHint) PanLaunchingHint.Visibility = Visibility.Visible;
animList.AddRange(new[]
{
ModAnimation.AaOpacity(LabLaunchingProgress, (!isLaunched ? 1 : 0) - LabLaunchingProgress.Opacity,
100),
ModAnimation.AaOpacity(LabLaunchingProgressLeft,
(!isLaunched ? 0.5d : 0d) - LabLaunchingProgressLeft.Opacity, 100),
ModAnimation.AaOpacity(PanLaunchingHint,
(isLaunched && shouldShowHint ? 1 : 0) - PanLaunchingHint.Opacity, 100)
});
}
ModAnimation.AniStart(animList, "Launching Progress");
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Minecraft.Launch.Error.RefreshInfo"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Minecraft.Launch.Error.RefreshInfo"));
}
}
private void PanLaunchingInfo_SizeChangedW(object sender, SizeChangedEventArgs e)
{
var deltaWidth = e.NewSize.Width - e.PreviousSize.Width;
if (e.PreviousSize.Width == 0d || isWidthAnimating || Math.Abs(deltaWidth) < 1d ||
PanLaunchingInfo.ActualWidth == 0d)
return;
ModAnimation.AniStart(new[]
{
ModAnimation.AaWidth(PanLaunchingInfo, deltaWidth, 180, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaCode(() =>
{
isWidthAnimating = false;
PanLaunchingInfo.Width = actualUsedWidth;
}, after: true)
}, "Launching Info Width");
isWidthAnimating = true;
actualUsedWidth = PanLaunchingInfo.Width;
PanLaunchingInfo.Width = e.PreviousSize.Width;
}
private void PanLaunchingInfo_SizeChangedH(object sender, SizeChangedEventArgs e)
{
var deltaHeight = e.NewSize.Height - e.PreviousSize.Height;
if (e.PreviousSize.Height == 0d || isHeightAnimating || Math.Abs(deltaHeight) < 1d ||
PanLaunchingInfo.ActualHeight == 0d)
return;
ModAnimation.AniStart(new[]
{
ModAnimation.AaHeight(PanLaunchingInfo, deltaHeight, 180, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaCode(() =>
{
isHeightAnimating = false;
PanLaunchingInfo.Height = actualUsedHeight;
}, after: true)
}, "Launching Info Height");
isHeightAnimating = true;
actualUsedHeight = PanLaunchingInfo.Height;
PanLaunchingInfo.Height = e.PreviousSize.Height;
}
// 启动游戏按钮
private void BtnLaunch_Click(object sender, MouseButtonEventArgs e)
{
LaunchButtonClick();
}
#region
/// <summary>
/// 切换至启动中页面。
/// </summary>
public void PageChangeToLaunching()
{
// 修改验证方式
switch (ModProfile.selectedProfile.Type)
{
case ModLaunch.McLoginType.Legacy:
{
LabLaunchingMethod.Text = Lang.Text("Launch.Account.Type.Offline");
break;
}
case ModLaunch.McLoginType.Ms:
{
LabLaunchingMethod.Text = Lang.Text("Launch.Account.Type.Microsoft");
break;
}
case ModLaunch.McLoginType.Auth:
{
LabLaunchingMethod.Text = Lang.Text("Launch.Account.Type.ThirdParty") + (!string.IsNullOrEmpty(ModProfile.selectedProfile.ServerName)
? " / " + ModProfile.selectedProfile.ServerName
: "");
break;
}
}
// 初始化页面
LabLaunchingName.Text = ModInstanceList.McMcInstanceSelected.Name;
LabLaunchingStage.Text = Lang.Text("Common.Action.Initialize");
LabLaunchingTitle.Text = ModLaunch.currentLaunchOptions?.SaveBatch is null
? Lang.Text("Launch.Status.Title.Launching")
: Lang.Text("Launch.Status.Title.ExportingScript");
LabLaunchingProgress.Text = Lang.Number(0d, "P2");
LabLaunchingProgress.Opacity = 1d;
LabLaunchingDownload.Visibility = Visibility.Visible;
LabLaunchingProgressLeft.Opacity = 0.6d;
LabLaunchingDownload.Visibility = Visibility.Visible;
LabLaunchingDownload.Text = ModBase.GetString(0) + "/s";
LabLaunchingDownload.Opacity = 0d;
LabLaunchingDownload.Visibility = Visibility.Collapsed;
LabLaunchingDownloadLeft.Opacity = 0d;
LabLaunchingDownloadLeft.Visibility = Visibility.Collapsed;
ProgressLaunchingFinished.Width = new GridLength(0d, GridUnitType.Star);
ProgressLaunchingUnfinished.Width = new GridLength(1d, GridUnitType.Star);
PanLaunchingHint.Opacity = 0d;
PanLaunchingHint.Visibility = Visibility.Collapsed;
PanLaunchingInfo.Width = double.NaN; // 重置宽度改变动画
ModLaunch.mcLaunchProcess = null;
ModLaunch.mcLaunchWatcher = null;
var shouldShowHint = Config.Preference.ShowLaunchingHint;
if (shouldShowHint)
LabLaunchingHint.Text = PageLaunchRight.GetRandomHint(true, true);
else
LabLaunchingHint.Text = "";
// 初始化其他页面
PanInput.IsHitTestVisible = false;
PanLaunching.IsHitTestVisible = false;
LoadLaunching.State.LoadingState = MyLoading.MyLoadingState.Run;
PanLaunching.Visibility = Visibility.Visible;
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(PanInput, 0d, 50),
ModAnimation.AaOpacity(PanInput, -PanInput.Opacity, 110, ease: new ModAnimation.AniEaseInFluent(),
after: true),
ModAnimation.AaScaleTransform(PanInput, 1.2d - ((ScaleTransform)PanInput.RenderTransform).ScaleX, 160),
ModAnimation.AaOpacity(PanLaunching, 1d - PanLaunching.Opacity, 150, 100),
ModAnimation.AaScaleTransform(PanLaunching, 1d - ((ScaleTransform)PanLaunching.RenderTransform).ScaleX,
500, 100, new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaCode(() => PanLaunching.IsHitTestVisible = true, 150)
}, "Launch State Page"); // 略作延迟,这样如果预检测失败,不会出现奇怪的弹一下的动画
}
/// <summary>
/// 切换至登录页面。
/// </summary>
public void PageChangeToLogin()
{
if (PageGet(pageCurrent) is ILoginPage loginPage) loginPage.Reload();
PanInput.IsHitTestVisible = false;
PanLaunching.IsHitTestVisible = false;
LoadLaunching.State.LoadingState = MyLoading.MyLoadingState.Stop;
PanInput.Visibility = Visibility.Visible;
ModAnimation.AniStart(
new[]
{
ModAnimation.AaOpacity(PanLaunching, -PanLaunching.Opacity, 150),
ModAnimation.AaScaleTransform(PanLaunching,
0.8d - ((ScaleTransform)PanLaunching.RenderTransform).ScaleX, 150,
ease: new ModAnimation.AniEaseOutFluent(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaOpacity(PanInput, 1d - PanInput.Opacity, 250, 50),
ModAnimation.AaScaleTransform(PanInput, 1d - ((ScaleTransform)PanInput.RenderTransform).ScaleX, 300, 50,
new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak)),
ModAnimation.AaCode(() => PanInput.IsHitTestVisible = true, 200)
}, "Launch State Page", true);
}
#endregion
#region
private enum PageType
{
None,
Auth,
Ms,
Profile,
ProfileSkin,
Offline
}
/// <summary>
/// 当前页面的种类。
/// </summary>
private PageType pageCurrent = PageType.None;
private object PageGet(PageType type)
{
switch (type)
{
case PageType.Auth:
{
if (ModMain.frmLoginAuth is null)
ModMain.frmLoginAuth = new PageLoginAuth();
return ModMain.frmLoginAuth;
}
case PageType.Ms:
{
if (ModMain.frmLoginMs is null)
ModMain.frmLoginMs = new PageLoginMs();
return ModMain.frmLoginMs;
}
case PageType.Profile:
{
if (ModMain.frmLoginProfile is null)
ModMain.frmLoginProfile = new PageLoginProfile();
return ModMain.frmLoginProfile;
}
case PageType.ProfileSkin:
{
if (ModMain.frmLoginProfileSkin is null)
ModMain.frmLoginProfileSkin = new PageLoginProfileSkin();
return ModMain.frmLoginProfileSkin;
}
case PageType.Offline:
{
if (ModMain.frmLoginOffline is null)
ModMain.frmLoginOffline = new PageLoginOffline();
return ModMain.frmLoginOffline;
}
default:
{
throw new ArgumentOutOfRangeException("Type", "即将切换的登录分页编号越界");
}
}
}
/// <summary>
/// 切换现有登录页面种类,返回新页面的实例。
/// </summary>
/// <param name="type">新页面的种类。</param>
/// <param name="anim">是否显示动画。</param>
private object PageChange(PageType type, bool anim)
{
object pageNew = ModMain.frmLoginMs; // 初始化一个东西,避免在执行时出现异常导致雪崩
try
{
#region
if (pageCurrent == type)
return pageNew;
pageNew = PageGet(type);
#endregion
#region
ModAnimation.AniStop("FrmLogin PageChange");
// 清除页面关联性
if (pageNew is FrameworkElement element && element.Parent is not null)
{
element.SetValue(ContentPresenter.ContentProperty, null);
}
if (anim)
{
// 动画
// 执行动画
Dispatcher.Invoke(() => ModAnimation.AniStart(new[]
{
ModAnimation.AaOpacity(PanLogin, -PanLogin.Opacity, 100, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaCode(() =>
{
ModAnimation.AniControlEnabled += 1;
PanLogin.Children.Clear();
PanLogin.Children.Add((UIElement)pageNew);
ModAnimation.AniControlEnabled -= 1;
}, 100),
ModAnimation.AaOpacity(PanLogin, 1d, 100, 120, new ModAnimation.AniEaseInFluent())
}, "FrmLogin PageChange"), DispatcherPriority.Render);
}
else
{
// 无动画
ModAnimation.AniControlEnabled += 1;
PanLogin.Children.Clear();
PanLogin.Children.Add((UIElement)pageNew);
ModAnimation.AniControlEnabled -= 1;
}
#endregion
pageCurrent = type;
return pageNew;
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Account.Error.SwitchPage", ModBase.GetStringFromEnum(type)),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Launch.Account.Error.SwitchPage", ModBase.GetStringFromEnum(type)));
return pageNew;
}
}
/// <summary>
/// 确认当前显示的子页面正确,并刷新该页面。
/// </summary>
/// <param name="anim">是否显示动画</param>
/// <param name="targetLoginType">目标验证方式,若正在创建档案需填</param>
public void RefreshPage(bool anim, ModLaunch.McLoginType targetLoginType = default)
{
var type = default(PageType);
if (targetLoginType != default)
{
if (targetLoginType == ModLaunch.McLoginType.Ms)
type = PageType.Ms;
if (targetLoginType == ModLaunch.McLoginType.Auth)
type = PageType.Auth;
if (targetLoginType == ModLaunch.McLoginType.Legacy)
type = PageType.Offline;
}
else if (ModProfile.selectedProfile is not null)
{
type = PageType.ProfileSkin;
BtnLaunch.IsEnabled = true;
}
else
{
type = PageType.Profile;
if (_launchButtonAction != LaunchButtonAction.Download)
BtnLaunch.IsEnabled = false;
}
// 刷新页面
if (pageCurrent == type)
return;
PageChange(type, anim);
}
#endregion
#region
// 正版皮肤
public static ModLoader.LoaderTask<ModBase.EqualableList<string>, string> skinMs = new("Loader Skin Ms", SkinMsLoad,
SkinMsInput, ThreadPriority.AboveNormal);
private static ModBase.EqualableList<string> SkinMsInput()
{
// 获取名称
return new ModBase.EqualableList<string>
{ ModProfile.selectedProfile.Username, ModProfile.selectedProfile.Uuid };
}
private static void SkinMsLoad(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> data)
{
// 清空已有皮肤
// 如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载
ModBase.RunInUi(() =>
{
if (ModMain.frmLoginProfileSkin is not null && ModMain.frmLoginProfileSkin.Skin is not null)
ModMain.frmLoginProfileSkin.Skin.Clear();
});
// 获取 Url
var userName = data.input[0];
var uuid = data.input[1];
if (ModProfile.selectedProfile is not null)
{
userName = ModProfile.selectedProfile.Username;
uuid = ModProfile.selectedProfile.Uuid;
}
if (string.IsNullOrEmpty(userName))
{
data.output = ModBase.pathImage + "Skins/" + ModSkin.McSkinSex(ModProfile.GetOfflineUuid(userName)) +
".png";
ModBase.Log("[Minecraft] 获取微软正版皮肤失败,ID 为空");
goto Finish;
}
try
{
var result = ModSkin.McSkinGetAddress(uuid, "Ms");
if (data.IsAborted)
throw new ThreadInterruptedException("当前任务已取消:" + userName);
result = ModSkin.McSkinDownload(result);
if (data.IsAborted)
throw new ThreadInterruptedException("当前任务已取消:" + userName);
data.output = result;
}
catch (Exception ex)
{
if (ex is ThreadInterruptedException)
{
data.output = "";
ModBase.Log("[Minecraft] 已取消皮肤获取:" + userName);
return;
}
if (ex.ToString().Contains("429"))
{
data.output = ModBase.pathImage + "Skins/" +
ModSkin.McSkinSex(ModProfile.GetOfflineUuid(userName)) + ".png";
ModBase.Log(
Lang.Text("Launch.Skin.Error.MsRateLimited", userName),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Skin.Error.MsRateLimited", userName));
}
else if (ex.ToString().Contains("未设置自定义皮肤"))
{
data.output = ModBase.pathImage + "Skins/" +
ModSkin.McSkinSex(ModProfile.GetOfflineUuid(userName)) + ".png";
ModBase.Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载");
}
else
{
data.output = ModBase.pathImage + "Skins/" +
ModSkin.McSkinSex(ModProfile.GetOfflineUuid(userName)) + ".png";
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Error.MsGet", userName),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Skin.Error.MsGet", userName));
}
}
Finish: ;
// 刷新显示
if (ModMain.frmLoginProfileSkin is not null && ReferenceEquals(ModMain.frmLoginProfileSkin.Skin.loader, data))
ModBase.RunInUi(ModMain.frmLoginProfileSkin.Skin.Load);
else if (!data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新
data.input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
}
// 离线皮肤
public static ModLoader.LoaderTask<ModBase.EqualableList<string>, string> skinLegacy = new("Loader Skin Legacy",
SkinLegacyLoad, SkinLegacyInput, ThreadPriority.AboveNormal);
private static ModBase.EqualableList<string> SkinLegacyInput()
{
return new ModBase.EqualableList<string>
{ ModProfile.selectedProfile.Username, ModProfile.selectedProfile.Uuid };
}
private static void SkinLegacyLoad(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> data)
{
// 清空已有皮肤
ModBase.RunInUi(() =>
{
if (ModMain.frmLoginProfileSkin is not null && ModMain.frmLoginProfileSkin.Skin is not null)
ModMain.frmLoginProfileSkin.Skin.Clear();
});
data.output = ModBase.pathImage + "Skins/" + ModSkin.McSkinSex(data.input[1]) + ".png";
// 刷新显示
if (ModMain.frmLoginProfileSkin is not null && ReferenceEquals(ModMain.frmLoginProfileSkin.Skin.loader, data))
ModBase.RunInUi(() => ModMain.frmLoginProfileSkin.Skin.Load());
else if (!data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新
data.input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
}
// Authlib-Injector 皮肤
public static ModLoader.LoaderTask<ModBase.EqualableList<string>, string> skinAuth = new("Loader Skin Auth",
SkinAuthLoad, SkinAuthInput, ThreadPriority.AboveNormal);
private static ModBase.EqualableList<string> SkinAuthInput()
{
// 获取名称
return new ModBase.EqualableList<string>
{ ModProfile.selectedProfile.Username, ModProfile.selectedProfile.Uuid };
}
private static void SkinAuthLoad(ModLoader.LoaderTask<ModBase.EqualableList<string>, string> data)
{
// 清空已有皮肤
// 如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载
ModBase.RunInUi(() =>
{
if (ModMain.frmLoginProfileSkin is not null && ModMain.frmLoginProfileSkin.Skin is not null)
ModMain.frmLoginProfileSkin.Skin.Clear();
});
// 获取 Url
var userName = data.input[0];
var uuid = data.input[1];
if (string.IsNullOrEmpty(userName))
{
data.output = ModBase.pathImage + "Skins/Steve.png";
ModBase.Log("[Minecraft] 获取 Authlib-Injector 皮肤失败,ID 为空");
goto Finish;
}
try
{
var result = ModSkin.McSkinGetAddress(uuid, "Auth");
if (data.IsAborted)
throw new ThreadInterruptedException("当前任务已取消:" + userName);
result = ModSkin.McSkinDownload(result);
if (data.IsAborted)
throw new ThreadInterruptedException("当前任务已取消:" + userName);
data.output = result;
}
catch (Exception ex)
{
if (ex is ThreadInterruptedException)
{
data.output = "";
return;
}
if (ex.ToString().Contains("429"))
{
data.output = ModBase.pathImage + "Skins/Steve.png";
ModBase.Log(
$"[Minecraft] 获取 Authlib-Injector 皮肤失败({userName}):获取皮肤太过频繁,请 5 分钟后再试!",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Skin.Error.AuthlibRateLimited"));
}
else if (ex.ToString().Contains("未设置自定义皮肤"))
{
data.output = ModBase.pathImage + "Skins/Steve.png";
ModBase.Log("[Minecraft] 用户未设置自定义皮肤,跳过皮肤加载");
}
else
{
data.output = ModBase.pathImage + "Skins/Steve.png";
ModBase.Log(
ex,
Lang.Text("Launch.Skin.Error.AuthGet", userName),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Launch.Skin.Error.AuthGet", userName));
}
}
Finish: ;
// 刷新显示
if (ModMain.frmLoginProfileSkin is not null && ReferenceEquals(ModMain.frmLoginProfileSkin.Skin.loader, data))
ModBase.RunInUi(ModMain.frmLoginProfileSkin.Skin.Load);
else if (!data.IsAborted) // 如果已经中断,Input 也被清空,就不会再次刷新
data.input = null; // 清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
}
// 全部皮肤加载器
// 需要放在其中元素的后面,否则会因为它提前被加载而莫名其妙变成 Nothing
public static List<ModLoader.LoaderTask<ModBase.EqualableList<string>, string>> skinLoaders = new()
{ skinMs, skinLegacy, skinAuth };
#endregion
}
@@ -0,0 +1,24 @@
<local:MyPageRight x:Class="PCL.PageLaunchRight"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PCL"> <!-- 不知道为啥只有这个文件不能在 XAML 设置 PanScroll -->
<local:MyScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" x:Name="PanBack">
<StackPanel Name="PanMain" Margin="25,25,25,10" Grid.IsSharedSizeScope="True">
<StackPanel x:Name="PanCustom" />
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Launch.Right.CommunityHint.Title}" x:Name="PanHint">
<local:MyIconButton LogoScale="1.1"
SvgIcon="lucide/x"
Height="20" Width="20" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Top"
x:Name="BtnHintClose"
Click="BtnHintClose_Click" />
<StackPanel Margin="25,38,23,15">
<TextBlock x:Name="LabHint1" TextWrapping="Wrap" Margin="0,0,0,2" FontSize="13.5" />
<TextBlock x:Name="LabHint2" TextWrapping="Wrap" Margin="0,0,0,2" FontSize="13.5" />
</StackPanel>
</local:MyCard>
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Launch.Right.Log.Title}" x:Name="PanLog">
<TextBlock x:Name="LabLog" Margin="20,38,20,18" TextTrimming="None" TextWrapping="Wrap" />
</local:MyCard>
</StackPanel>
</local:MyScrollViewer>
</local:MyPageRight>
@@ -0,0 +1,898 @@
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
}
@@ -0,0 +1,56 @@
<Grid x:Class="PCL.PageLoginAuth" Tag="False"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PCL"
mc:Ignorable="d" d:DesignWidth="317.6" Margin="0,0,0,-2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="20" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock x:Name="TextServerName" Text="{DynamicResource Launch.Account.Auth.ServerLabel}" Grid.Row="0"
Grid.Column="1" Margin="-40,0,0,0"
HorizontalAlignment="Center" Visibility="Hidden" />
<local:MyIconButton ToolTip="{DynamicResource Launch.Account.Auth.ChangeProfile}" Grid.Row="4" Height="14"
Opacity="0" x:Name="BtnEdit"
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="32"
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" Grid.Column="0"
HorizontalAlignment="Center"
SvgIcon="lucide/pencil"
Margin="8,0,0,0" VerticalAlignment="Center" />
<local:MyIconButton ToolTip="{DynamicResource Launch.Account.Auth.Logout}" Grid.Row="4" Height="14" Opacity="0"
x:Name="BtnExit"
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="32"
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50" Grid.Column="2"
HorizontalAlignment="Center"
SvgIcon="lucide/log-out"
Margin="0,0,8,0" VerticalAlignment="Center" />
<local:MyComboBox Grid.Row="2" Grid.Column="1" Height="28" x:Name="TextServer" IsEditable="True"
IsTextSearchEnabled="False" />
<TextBlock Text="{DynamicResource Launch.Account.Auth.Server}" Grid.Row="2" VerticalAlignment="Center" />
<local:MyTextBox Grid.Row="4" Grid.Column="1" Height="28" x:Name="TextName" />
<TextBlock Text="{DynamicResource Launch.Account.Auth.Email}" Grid.Row="4" VerticalAlignment="Center" />
<PasswordBox Grid.Row="6" Grid.Column="1" x:Name="TextPass" />
<TextBlock Text="{DynamicResource Launch.Account.Auth.Password}" Grid.Row="6" VerticalAlignment="Center" />
<local:MyTextButton Margin="0,12,2,0" x:Name="BtnLink" Content="{DynamicResource Launch.Account.Auth.Register}"
Grid.Row="7" HorizontalAlignment="Right"
Grid.Column="1" Padding="20,0,0,0" HorizontalContentAlignment="Right" FontSize="13"
VerticalContentAlignment="Center" Visibility="Collapsed" />
<local:MyButton x:Name="BtnBack" Grid.Row="9" Grid.Column="1" Width="50" Margin="-100,0,0,0" ColorType="Normal"
Text="{DynamicResource Launch.Account.Auth.Back}" HorizontalAlignment="Center" />
<local:MyButton x:Name="BtnLogin" Grid.Row="9" Grid.Column="1" Width="50" Margin="20,0,0,0" ColorType="Highlight"
Text="{DynamicResource Launch.Account.Auth.Login}" HorizontalAlignment="Center" />
</Grid>
@@ -0,0 +1,210 @@
using System.Windows;
using System.Windows.Controls;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.IO.Net.Http;
using PCL.Core.Minecraft.Yggdrasil;
using PCL.Core.Utils;
using PCL.Core.Utils.Exts;
using PCL.Core.Utils.Validate;
namespace PCL;
public partial class PageLoginAuth
{
public static string draggedAuthServer;
// 预设服务器
private static readonly Dictionary<string, string> predefinedAuthServers = new()
{
{ Lang.Text("Launch.Account.Auth.Preset.LittleSkin"), "https://littleskin.cn/api/yggdrasil" },
{ Lang.Text("Common.Option.Customize"), "" }
};
private bool _isRegisterMode = true;
public PageLoginAuth()
{
InitializeComponent();
Loaded += (_, _) => Reload();
Loaded += (_, _) => ReloadRegisterButton();
// Handles
BtnBack.Click += BtnBack_Click;
BtnLogin.Click += BtnLogin_Click;
TextServer.TextChanged += TextServer_TextChanged;
BtnLink.Click += Btn_Click;
}
private void Reload()
{
var serverItems = TextServer.Items;
serverItems.Clear();
foreach (var serverName in predefinedAuthServers.Keys)
serverItems.Add(new MyComboBoxItem { Content = serverName });
TextServer.Text = draggedAuthServer;
draggedAuthServer = null;
}
private void BtnBack_Click(object sender, EventArgs e)
{
TextServer.Text = null;
TextName.Text = null;
TextPass.Password = null;
ModMain.frmLaunchLeft.RefreshPage(true);
}
private void BtnLogin_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(TextServer.Text) || string.IsNullOrWhiteSpace(TextName.Text) ||
string.IsNullOrWhiteSpace(TextPass.Password))
{
HintService.Hint(Lang.Text("Launch.Account.Auth.EmptyFields"), HintType.Error);
return;
}
if (!TextServer.Text.IsMatch(RegexPatterns.HttpUri))
{
HintService.Hint(Lang.Text("Launch.Account.Auth.InvalidServer"), HintType.Error);
return;
}
BtnLogin.IsEnabled = false;
BtnBack.IsEnabled = false;
var loginData = new ModLaunch.McLoginServer(ModLaunch.McLoginType.Auth)
{
BaseUrl = TextServer.Text.EndsWithF("/") ? $"{TextServer.Text}authserver" : $"{TextServer.Text}/authserver",
UserName = TextName.Text, Password = TextPass.Password, Description = "Authlib-Injector",
LoginType = ModLaunch.McLoginType.Auth
};
Dispatcher.BeginInvoke(new Func<Task>(async () =>
{
try
{
ModProfile.isCreatingProfile = true;
ModLaunch.mcLoginAuthLoader.Start(loginData, true);
while (ModLaunch.mcLoginAuthLoader.State == ModBase.LoadState.Loading)
{
BtnLogin.Text = Lang.Number(ModLaunch.mcLoginAuthLoader.Progress, "P0");
await Task.Delay(50);
}
switch (ModLaunch.mcLoginAuthLoader.State)
{
case ModBase.LoadState.Finished:
ModMain.frmLaunchLeft.RefreshPage(true);
break;
case ModBase.LoadState.Aborted:
HintService.Hint(Lang.Text("Launch.Account.Auth.Cancelled"));
break;
case ModBase.LoadState.Waiting:
case ModBase.LoadState.Loading:
case ModBase.LoadState.Failed:
default:
{
if (ModLaunch.mcLoginAuthLoader.Error is null)
throw new Exception(Lang.Text("Launch.Account.Microsoft.Error.Unknown"));
throw new Exception(ModLaunch.mcLoginAuthLoader.Error.Message,
ModLaunch.mcLoginAuthLoader.Error);
}
}
}
catch (Exception ex)
{
if (ex.Message == "$$")
{
}
else if (ex.Message.StartsWith("$"))
{
HintService.Hint(
Lang.Text("Launch.Account.Auth.LoginFailed.WithDetail",ex.Message.TrimStart('$')),
HintType.Error);
}
else
{
ModBase.Log(
ex,
Lang.Text("Launch.Account.Auth.LoginFailed"),
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Launch.Account.Auth.LoginFailed"));
}
}
finally
{
ModProfile.isCreatingProfile = false;
BtnLogin.IsEnabled = true;
BtnBack.IsEnabled = true;
BtnLogin.Text = Lang.Text("Launch.Account.Auth.Login");
}
}));
}
// 获取验证服务器名称
private void GetServerName()
{
var serverUriInput = TextServer.Text;
if (string.IsNullOrWhiteSpace(serverUriInput))
{
TextServerName.Visibility = Visibility.Hidden;
return;
}
Dispatcher.BeginInvoke(async () =>
{
string serverUri = null;
string serverName = null;
try
{
serverUri = await ApiLocation.TryRequestAsync(serverUriInput);
using var resp = await HttpRequest.Create(serverUri).SendAsync();
var responseText = await resp.AsStringAsync();
serverName = await Task.Run(() => ModBase.GetJson(responseText)["meta"]?["serverName"]?.ToString());
}
catch (Exception ex)
{
ModBase.Log(ex, "从服务器获取名称失败");
}
if (serverUri is not null) TextServer.Text = serverUri;
if (serverName is null)
{
TextServerName.Visibility = Visibility.Hidden;
}
else
{
TextServerName.Text = Lang.Text("Launch.Account.Auth.ServerLabel", serverName);
TextServerName.Visibility = Visibility.Visible;
}
});
}
// 链接处理
private void ComboName_TextChanged(object sender, TextChangedEventArgs e)
{
_isRegisterMode = string.IsNullOrEmpty(TextName.Text);
BtnLink.Content = _isRegisterMode
? Lang.Text("Launch.Account.Auth.Register")
: Lang.Text("Launch.Account.Auth.ForgotPassword");
}
private void Btn_Click(object sender, EventArgs e)
{
ModBase.OpenWebsite(_isRegisterMode
? Config.InstanceAuth.AuthRegisterAddress.ToString()
: Config.InstanceAuth.AuthRegisterAddress.ToString().Replace("/auth/register", "/auth/forgot"));
}
// 切换注册按钮可见性
private void ReloadRegisterButton()
{
var address = Config.InstanceAuth.AuthRegisterAddress.ToString();
BtnLink.Visibility = new HttpValidator().Validate(address).IsValid
? Visibility.Visible
: Visibility.Collapsed;
}
private void TextServer_TextChanged(object sender, TextChangedEventArgs e)
{
predefinedAuthServers.TryGetValue(TextServer.Text, out var server);
if (server is not null) TextServer.Text = server;
}
}
@@ -0,0 +1,45 @@
<StackPanel x:Class="PCL.PageLoginMs" Tag="False" Orientation="Vertical"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PCL"
mc:Ignorable="d" d:DesignWidth="302.4">
<Grid x:Name="PanEmpty" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="25" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Path
Data="M660.338 528.065c63.61-46.825 105.131-121.964 105.131-206.83 0-141.7-115.29-256.987-256.997-256.987-141.706 0-256.998 115.288-256.998 256.987 0 85.901 42.52 161.887 107.456 208.562-152.1 59.92-260.185 207.961-260.185 381.077 0 21.276 17.253 38.53 38.53 38.53 21.278 0 38.53-17.254 38.53-38.53 0-183.426 149.232-332.671 332.667-332.671 1.589 0 3.113-0.207 4.694-0.244 0.8 0.056 1.553 0.244 2.362 0.244 183.434 0 332.664 149.245 332.664 332.671 0 21.276 17.255 38.53 38.533 38.53 21.277 0 38.53-17.254 38.53-38.53 0-174.885-110.354-324.13-264.917-382.809z m-331.803-206.83c0-99.22 80.72-179.927 179.935-179.927s179.937 80.708 179.937 179.927c0 99.203-80.721 179.91-179.937 179.91s-179.935-80.708-179.935-179.91z"
Fill="{DynamicResource ColorBrush2}" Opacity="0.7" Width="40" Stretch="Uniform" Grid.ColumnSpan="2"
Margin="4,0,0,0" />
<local:MyButton Text="{DynamicResource Launch.Account.Microsoft.Start}" x:Name="BtnLogin" ColorType="Highlight"
Height="28" Width="100" Margin="0,0,0,0" Grid.Row="2" Grid.Column="0" />
<Grid Grid.Row="4" Grid.ColumnSpan="2" Opacity="0.3" Margin="2,30,2,0" HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="30" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<local:MyTextButton Text="{DynamicResource Launch.Account.Microsoft.Purchase}"
local:CustomEventService.EventType="OpenUrl"
local:CustomEventService.EventData="https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj"
Grid.Column="0" />
<local:MyTextButton Text="{DynamicResource Launch.Account.Microsoft.Website}"
local:CustomEventService.EventType="OpenUrl"
local:CustomEventService.EventData="https://www.minecraft.net/zh-hans"
Grid.Column="2" />
</Grid>
<local:MyTextButton x:Name="BtnBack" Text="{DynamicResource Launch.Account.Back}" Grid.Row="5"
Grid.ColumnSpan="2" HorizontalAlignment="Center"
Margin="0,60,0,0" Opacity="0.3" />
</Grid>
</StackPanel>
@@ -0,0 +1,91 @@
using System.Security.Authentication;
using System.Windows;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageLoginMs
{
public PageLoginMs()
{
// Handles
InitializeComponent();
BtnBack.Click += BtnBack_Click;
BtnLogin.Click += BtnLogin_Click;
}
private void BtnBack_Click(object sender, EventArgs e)
{
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true));
}
private void BtnLogin_Click(object sender, EventArgs e)
{
BtnLogin.IsEnabled = false;
BtnBack.Visibility = Visibility.Collapsed;
BtnLogin.Text = Lang.Number(0d, "P0");
ModBase.RunInNewThread(() =>
{
try
{
ModProfile.selectedProfile = null;
ModLaunch.mcLoginMsLoader.Start(ModProfile.GetLoginData(ModLaunch.McLoginType.Ms), true);
while (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Loading)
{
ModBase.RunInUi(() => BtnLogin.Text = Lang.Number(ModLaunch.mcLoginMsLoader.Progress, "P0"));
Thread.Sleep(50);
}
if (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Finished)
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true));
else if (ModLaunch.mcLoginMsLoader.State == ModBase.LoadState.Aborted)
throw new ThreadInterruptedException();
else if (ModLaunch.mcLoginMsLoader.Error is null)
throw new Exception(Lang.Text("Launch.Account.Microsoft.Error.Unknown"));
else
throw new Exception(ModLaunch.mcLoginMsLoader.Error.Message, ModLaunch.mcLoginMsLoader.Error);
}
catch (ThreadInterruptedException ex)
{
HintService.Hint(Lang.Text("Launch.Account.LoginCancelled"));
}
catch (Exception ex)
{
if (ex.Message == "$$")
{
}
else if (ex.Message.StartsWith("$"))
{
HintService.Hint(
Lang.Text("Launch.Account.Microsoft.LoginFailed.WithDetail", ex.Message.TrimStart('$')),
HintType.Error);
}
else if (ex is AuthenticationException && ex.Message.ContainsF("SSL/TLS"))
{
ModBase.Log(
ex,
$"{Lang.Text("Launch.Account.Microsoft.LoginFailed.Message")}\r\n{ex.Message}",
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Launch.Account.Microsoft.Error.OperationFailed"));
}
else
{
ModBase.Log(
ex,
Lang.Text("Launch.Account.Microsoft.LoginFailed.Title"),
ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Launch.Account.Microsoft.LoginFailed.Title"));
}
}
finally
{
ModBase.RunInUi(() =>
{
BtnLogin.IsEnabled = true;
BtnBack.Visibility = Visibility.Visible;
BtnLogin.Text = Lang.Text("Launch.Account.Login");
});
}
}, "Ms Login");
}
}
@@ -0,0 +1,58 @@
<Grid x:Class="PCL.PageLoginOffline" Tag="False"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PCL"
mc:Ignorable="d" d:DesignWidth="317.6" Margin="0,0,0,-2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="30" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="50" />
<RowDefinition Height="Auto" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{DynamicResource Launch.Account.Offline.PlayerId}" Grid.Row="0" HorizontalAlignment="Center"
VerticalAlignment="Center" />
<local:MyTextBox Grid.Row="0" Grid.Column="1" Height="28" x:Name="TextName"
ToolTip="{DynamicResource Launch.Account.Offline.PlayerId.ToolTip}" />
<TextBlock Grid.Row="2" Grid.Column="1" Text="{DynamicResource Launch.Account.Offline.UuidStandard}"
TextAlignment="Center" Margin="-45,0,0,0" FontWeight="Bold" />
<Grid Margin="-40,0,0,0" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="85" />
<ColumnDefinition Width="85" />
<ColumnDefinition Width="85" />
</Grid.ColumnDefinitions>
<local:MyRadioBox Grid.Column="0" Text="{DynamicResource Launch.Account.Offline.Uuid.Standard}"
x:Name="RadioUuidStandard" Checked="True" Height="22"
HorizontalAlignment="Center" VerticalAlignment="Center" Check="RadioUuid_Checked"
ToolTip="{DynamicResource Launch.Account.Offline.Uuid.Standard.ToolTip}" />
<local:MyRadioBox Grid.Column="1" Text="{DynamicResource Launch.Account.Offline.Uuid.Legacy}"
x:Name="RadioUuidLegacy" Height="22" HorizontalAlignment="Center"
VerticalAlignment="Center" Check="RadioUuid_Checked"
ToolTip="{DynamicResource Launch.Account.Offline.Uuid.Legacy.ToolTip}" />
<local:MyRadioBox Grid.Column="2" Text="{DynamicResource Common.Option.Customize}" x:Name="RadioUuidCustom"
Height="22" HorizontalAlignment="Center"
VerticalAlignment="Center" Check="RadioUuid_Checked"
ToolTip="{DynamicResource Launch.Account.Offline.Uuid.Custom.ToolTip}" />
</Grid>
<TextBlock Text="UUID" Grid.Row="6" x:Name="TextUuidTitle" HorizontalAlignment="Center" VerticalAlignment="Center"
Visibility="Collapsed" />
<local:MyTextBox Grid.Row="6" Grid.Column="1" Height="28" Visibility="Collapsed" x:Name="TextUuid"
ToolTip="{DynamicResource Launch.Account.Offline.Uuid.Custom.Input.ToolTip}" />
<local:MyButton x:Name="BtnBack" Grid.Row="9" Grid.Column="1" Width="50" Margin="-100,-40,0,40" ColorType="Normal"
Text="{DynamicResource Launch.Account.Offline.Back}" HorizontalAlignment="Center" />
<local:MyButton x:Name="BtnLogin" Grid.Row="9" Grid.Column="1" Width="50" Margin="20,-40,0,40"
ColorType="Highlight" Text="{DynamicResource Launch.Account.Offline.Create}"
HorizontalAlignment="Center" />
</Grid>
@@ -0,0 +1,88 @@
using System.Windows;
using PCL.Core.Utils.Validate;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageLoginOffline
{
public PageLoginOffline()
{
// Handles
InitializeComponent();
BtnBack.Click += BtnBack_Click;
RadioUuidCustom.Check += RadioUuid_Checked;
RadioUuidStandard.Check += RadioUuid_Checked;
RadioUuidLegacy.Check += RadioUuid_Checked;
BtnLogin.Click += BtnLogin_Click;
}
private void BtnBack_Click(object sender, EventArgs e)
{
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true));
}
private void RadioUuid_Checked(object sender, ModBase.RouteEventArgs e)
{
if (RadioUuidCustom.Checked)
{
TextUuidTitle.Visibility = Visibility.Visible;
TextUuid.Visibility = Visibility.Visible;
}
else
{
TextUuidTitle.Visibility = Visibility.Collapsed;
TextUuid.Visibility = Visibility.Collapsed;
}
}
private void BtnLogin_Click(object sender, EventArgs e)
{
// 玩家 ID 输入检查
var username = TextName.Text;
var usernameValidateResult = new RegexValidator("^[A-z0-9_]{3,16}$").Validate(username);
if (!usernameValidateResult.IsValid)
if (ModMain.MyMsgBox(
Lang.Text("Launch.Account.Offline.InvalidPlayerId.Message"),
Lang.Text("Launch.Account.Offline.InvalidPlayerId.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel"), isWarn: true, forceWait: true) == 2)
return;
// UUID
string userUuid = null;
if (RadioUuidCustom.Checked)
{
// 自定义输入检查
var uuidInput = TextUuid.Text.Replace("-", "");
var uuidValidateResult = new RegexValidator("^[a-fA-F0-9]{32}$").Validate(uuidInput);
if (RadioUuidCustom.Checked && !uuidValidateResult.IsValid)
{
HintService.Hint(Lang.Text("Launch.Account.Offline.InvalidUuid", uuidValidateResult), HintType.Error);
return;
}
userUuid = uuidInput;
}
else if (RadioUuidLegacy.Checked)
{
userUuid = ModProfile.GetOfflineUuid(username, isLegacy: true);
}
else
{
userUuid = ModProfile.GetOfflineUuid(username);
}
// 创建档案
var newProfile = new ModProfile.McProfile
{
Type = ModLaunch.McLoginType.Legacy,
Uuid = userUuid,
Username = username,
Desc = ""
};
ModProfile.profileList.Add(newProfile);
ModProfile.SaveProfile();
ModProfile.selectedProfile = newProfile;
ModProfile.isCreatingProfile = false;
HintService.Hint(Lang.Text("Launch.Account.Profile.Created"), HintType.Success);
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(true));
}
}
@@ -0,0 +1,54 @@
<Grid x:Class="PCL.PageLoginProfile" Tag="False"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PCL"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
<RowDefinition Height="*" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<local:MyHint x:Name="HintSelect" Margin="0,10,0,0" Theme="Blue" Text="{DynamicResource Launch.Account.Profile.SelectHint}" Grid.Row="0" CanClose="True"
RelativeSetup="HintProfileSelect" />
<local:MyHint x:Name="HintCreate" Margin="0,10,0,0" Theme="Blue" Text="{DynamicResource Launch.Account.Profile.CreateAndSelectHint}" Grid.Row="0"
Visibility="Collapsed" />
<local:MyHint x:Name="HintMicrosoft" Margin="0,10,0,0" Theme="Yellow" Text="{DynamicResource Launch.Account.Profile.RequireMicrosoftHint}" Grid.Row="1"
Visibility="Collapsed" />
<local:MyScrollViewer Grid.Row="3" MaxHeight="300" Margin="10,0,10,0">
<!-- 世界需要 MVVM -->
<ItemsControl ItemsSource="{Binding ProfileCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:MyListItem Margin="8,2,10,2"
Title="{Binding Username}" Info="{Binding Info}"
Type="Clickable" Logo="{Binding Logo}" SvgIcon="{Binding SvgIcon}" Tag="{Binding Profile}"
Click="SelectProfile" ContentHandler="ProfileContMenuBuild" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</local:MyScrollViewer>
<local:MyCard Grid.Row="5" HorizontalAlignment="Center" x:Name="PanButtons" Opacity="10"
CornerRadius="5" Margin="0,0,4,0">
<StackPanel Orientation="Horizontal" Margin="10,3,0,3">
<local:MyIconButton Height="24" Margin="0,0.5,9,0" x:Name="BtnNew"
Click="BtnNew_Click"
ToolTip="{DynamicResource Launch.Account.Profile.Create}" ToolTipService.Placement="Center" ToolTipService.VerticalOffset="35"
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50"
LogoScale="1.08"
SvgIcon="lucide/user-plus" />
</StackPanel>
</local:MyCard>
</Grid>
@@ -0,0 +1,199 @@
using System.Collections.ObjectModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.UI;
namespace PCL;
public partial class PageLoginProfile
{
public PageLoginProfile()
{
InitializeComponent();
Loaded += (_, _) => Reload();
}
public ObservableCollection<ProfileItem> ProfileCollection { get; set; } = new();
/// <summary>
/// 刷新页面显示的所有信息。
/// </summary>
public void Reload()
{
RefreshProfileList();
ModMain.frmLoginProfileSkin = null;
// RunInNewThread(Sub()
// Thread.Sleep(800)
// RunInUi(Sub() FrmLaunchLeft.RefreshPage(True))
// End Sub)
}
/// <summary>
/// 刷新档案列表
/// </summary>
public void RefreshProfileList()
{
ModBase.Log("[Profile] 刷新档案列表");
ProfileCollection.Clear();
ModProfile.GetProfile();
try
{
foreach (var p in ModProfile.profileList)
ProfileCollection.Add(new ProfileItem(p));
HintMicrosoft.Visibility = ModProfile.profileList.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
ModBase.Log("[Profile] 档案列表刷新完成");
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Launch.Account.Profile.Error.Read"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Launch.Account.Profile.Error.Read"));
}
if (!ModProfile.profileList.Any())
{
States.Hint.LaunchWithProfile = true;
HintCreate.Visibility = Visibility.Visible;
}
else
{
HintCreate.Visibility = Visibility.Collapsed;
}
}
public class ProfileItem
{
public ProfileItem(ModProfile.McProfile profile)
{
Profile = profile;
Info = (string)ModProfile.GetProfileInfo(profile);
var logoPath = ModBase.pathTemp + $@"Cache\Skin\Head\{profile.SkinHeadId}.png";
if (File.Exists(logoPath) && new FileInfo(logoPath).Length != 0L)
{
Logo = logoPath;
SvgIcon = string.Empty;
}
else
{
Logo = string.Empty;
SvgIcon = "lucide/user";
}
}
public string Info { get; private set; }
public string Logo { get; private set; } = string.Empty;
public string SvgIcon { get; private set; } = string.Empty;
public ModProfile.McProfile Profile { get; }
public string Username => Profile.Username;
}
#region
private void SelectProfile(object sender, MouseButtonEventArgs e)
{
var item = (MyListItem)sender;
var tag = (ModProfile.McProfile)item.Tag;
ModProfile.selectedProfile = (ModProfile.McProfile)((MyListItem)sender).Tag;
ModBase.Log($"[Profile] 选定档案: {tag.Username}, 以 {tag.Type} 方式验证");
ModProfile.lastUsedProfile =
ModProfile.profileList.IndexOf((ModProfile.McProfile)((MyListItem)sender).Tag); // 获取当前档案的序号
ModProfile.SaveProfile(); // 保存档案配置,确保切换后的档案被正确保存
// 清除登录验证缓存,确保使用新档案的验证信息
ModLaunch.mcLoginMsLoader.State = ModBase.LoadState.Waiting;
ModLaunch.mcLoginAuthLoader.State = ModBase.LoadState.Waiting;
ModLaunch.mcLoginLegacyLoader.State = ModBase.LoadState.Waiting;
ModBase.RunInUi(() =>
{
ModMain.frmLaunchLeft.RefreshPage(true);
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = true;
});
}
private void ProfileContMenuBuild(MyListItem sender, EventArgs e)
{
// 更改 UUID
var btnEditUuid = new MyIconButton
{ SvgIcon = "lucide/pencil", ToolTip = Lang.Text("Launch.Account.Profile.ChangeUuid"), Tag = sender.Tag };
ToolTipService.SetPlacement(btnEditUuid, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnEditUuid, 30d);
ToolTipService.SetHorizontalOffset(btnEditUuid, 2d);
btnEditUuid.Click += EditProfileUuid;
// 复制 UUID
var btnCopyUuid = new MyIconButton
{ SvgIcon = "lucide/copy", ToolTip = Lang.Text("Launch.Account.Profile.CopyUuid"), Tag = sender.Tag };
ToolTipService.SetPlacement(btnCopyUuid, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnCopyUuid, 30d);
ToolTipService.SetHorizontalOffset(btnCopyUuid, 2d);
btnCopyUuid.Click += CopyProfileUuid;
// 更改验证服务器名称
var btnEditServerName = new MyIconButton
{ SvgIcon = "lucide/info", ToolTip = Lang.Text("Launch.Account.Profile.ChangeAuthServerName"), Tag = sender.Tag };
ToolTipService.SetPlacement(btnEditServerName, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnEditServerName, 30d);
ToolTipService.SetHorizontalOffset(btnEditServerName, 2d);
btnEditServerName.Click += EditProfileServer;
// 删除档案
var btnDelete = new MyIconButton { SvgIcon = "lucide/trash-2", ToolTip = Lang.Text("Launch.Account.Profile.Delete"), Tag = sender.Tag };
ToolTipService.SetPlacement(btnDelete, PlacementMode.Center);
ToolTipService.SetVerticalOffset(btnDelete, 30d);
ToolTipService.SetHorizontalOffset(btnDelete, 2d);
btnDelete.Click += DeleteProfile;
// 根据档案类型显示不同的菜单项
if (((ModProfile.McProfile)sender.Tag).Type == ModLaunch.McLoginType.Legacy)
sender.Buttons = new[] { btnEditUuid, btnDelete };
else
sender.Buttons = new[] { btnCopyUuid, btnDelete };
}
// 创建档案
private void BtnNew_Click(object sender, EventArgs e)
{
ModBase.RunInNewThread(() =>
{
ModProfile.CreateProfile();
ModBase.RunInUi(() => RefreshProfileList());
});
}
// 编辑 UUID
private void EditProfileUuid(object sender, EventArgs e)
{
ModProfile.EditOfflineUuid((ModProfile.McProfile)((MyIconButton)sender).Tag);
}
private void CopyProfileUuid(object sender, EventArgs e)
{
if (sender is MyIconButton { Tag: ModProfile.McProfile profile }) ModBase.ClipboardSet(profile.Uuid);
}
// 编辑验证服务器名称
private void EditProfileServer(object sender, EventArgs e)
{
var profile = (ModProfile.McProfile)((MyIconButton)sender).Tag;
string name = ModMain.MyMsgBoxInput(Lang.Text("Launch.Account.Profile.EditServerName.Title"), Lang.Text("Launch.Account.Profile.EditServerName.Message"), profile.ServerName);
if (name is not null) ModProfile.EditAuthServerName(profile, name);
}
// 删除档案
private void DeleteProfile(object sender, EventArgs e)
{
if (ModMain.MyMsgBox(Lang.Text("Launch.Account.Profile.DeleteConfirm.Message"), Lang.Text("Launch.Account.Profile.DeleteConfirm.Title"), Lang.Text("Common.Action.Continue"), Lang.Text("Common.Action.Cancel"), isWarn: true,
forceWait: true) == 2)
return;
ModProfile.RemoveProfile((ModProfile.McProfile)((MyIconButton)sender).Tag);
ModBase.RunInUi(() => RefreshProfileList());
}
#endregion
}
@@ -0,0 +1,75 @@
<Grid x:Class="PCL.PageLoginProfileSkin" Tag="False"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PCL"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="0,0,0,0" Background="{StaticResource ColorBrushTransparent}" Name="PanData">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="34" />
<ColumnDefinition />
<ColumnDefinition Width="34" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="80" />
<RowDefinition Height="Auto" MinHeight="30" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Grid.Column="1" Grid.Row="1" VerticalAlignment="Top">
<local:MySkin HorizontalAlignment="Center" VerticalAlignment="Top" x:Name="Skin" HasCape="False"
Margin="0,0,0,10" IsHitTestVisible="False" />
<TextBlock Name="TextName" Text="Username" Margin="8,0" FontSize="16" HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis" />
<TextBlock Name="TextType" Text="AuthType" Margin="8,4,8,5" FontSize="12" HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis" Foreground="{DynamicResource ColorBrushGray4}" />
</StackPanel>
<local:MyCard Grid.Column="1" Grid.Row="2" HorizontalAlignment="Center" x:Name="PanButtons" Opacity="0"
CornerRadius="5" Margin="0,8,0,0">
<StackPanel Orientation="Horizontal" Margin="10,3,9,3">
<local:MyIconButton Height="24" Margin="0,0.5,9,0" x:Name="BtnSkin"
ToolTip="{DynamicResource Launch.Account.ProfileSkin.SkinsAndCapes}"
ToolTipService.Placement="Center"
ToolTipService.VerticalOffset="35" ToolTipService.HorizontalOffset="1"
ToolTipService.InitialShowDelay="50"
LogoScale="1.08"
SvgIcon="lucide/shirt">
<local:MyIconButton.ContextMenu>
<ContextMenu Closed="MenuAccountOptions_Closed" HorizontalOffset="10" VerticalOffset="18">
<local:MyMenuItem Click="Skin_Click"
Header="{DynamicResource Launch.Account.ProfileSkin.ChangeSkin}" />
<local:MyMenuItem Click="BtnSkinSave_Click"
Header="{DynamicResource Launch.Account.ProfileSkin.SaveSkin}" />
<local:MyMenuItem Click="BtnSkinRefresh_Click"
Header="{DynamicResource Launch.Account.ProfileSkin.RefreshSkin}" />
<local:MyMenuItem Click="BtnSkinCape_Click"
Header="{DynamicResource Launch.Account.ProfileSkin.ChangeCape}" />
</ContextMenu>
</local:MyIconButton.ContextMenu>
</local:MyIconButton>
<local:MyIconButton Height="24" Margin="0,0,9,0" x:Name="BtnEdit"
ToolTip="{DynamicResource Launch.Account.ProfileSkin.EditProfile}"
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="35"
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50"
LogoScale="1.1"
SvgIcon="lucide/pencil">
<local:MyIconButton.ContextMenu>
<ContextMenu Closed="MenuAccountOptions_Closed" HorizontalOffset="10" VerticalOffset="18">
<local:MyMenuItem Click="BtnEditPassword_Click"
Header="{DynamicResource Launch.Account.ProfileSkin.EditPassword}" />
<local:MyMenuItem Click="BtnEditName_Click"
Header="{DynamicResource Launch.Account.ProfileSkin.EditUsername}" />
</ContextMenu>
</local:MyIconButton.ContextMenu>
</local:MyIconButton>
<local:MyIconButton Height="24" x:Name="BtnSelect" Margin="0,0,0,0" Click="ChangeProfile"
ToolTip="{DynamicResource Launch.Account.ProfileSkin.SwitchProfile}"
ToolTipService.Placement="Center" ToolTipService.VerticalOffset="35"
ToolTipService.HorizontalOffset="1" ToolTipService.InitialShowDelay="50"
LogoScale="1.15"
SvgIcon="lucide/arrow-right-left" />
</StackPanel>
</local:MyCard>
</Grid>
</Grid>
@@ -0,0 +1,159 @@
using System.Windows;
using System.Windows.Input;
using PCL.Core.App.Localization;
namespace PCL;
public partial class PageLoginProfileSkin
{
public PageLoginProfileSkin()
{
InitializeComponent();
Loaded += (_, _) => Reload();
// Handles
PanData.MouseEnter += ShowPanel;
PanData.MouseLeave += HidePanel;
BtnSkin.Click += BtnSkin_Click;
BtnEdit.Click += BtnEdit_Click;
BtnSelect.Click += ChangeProfile;
}
/// <summary>
/// 刷新页面显示的所有信息。
/// </summary>
public void Reload()
{
ModBase.Log("[Profile] 刷新档案界面");
Skin.Clear();
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Ms)
{
BtnEdit.Visibility = Visibility.Visible;
ModBase.Log("[Profile] 使用正版皮肤加载器");
Skin.loader = PageLaunchLeft.skinMs;
}
else if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Auth)
{
BtnEdit.Visibility = Visibility.Visible;
ModBase.Log("[Profile] 使用 Authlib 皮肤加载器");
Skin.loader = PageLaunchLeft.skinAuth;
}
else
{
BtnEdit.Visibility = Visibility.Collapsed;
ModBase.Log("[Profile] 使用离线皮肤加载器");
Skin.loader = PageLaunchLeft.skinLegacy;
}
Skin.loader.Start(isForceRestart: true);
TextName.Text = ModProfile.selectedProfile.Username;
TextType.Text = (string)ModProfile.GetProfileInfo(ModProfile.selectedProfile);
}
#region
// 显示 / 隐藏控制
private void ShowPanel(object sender, MouseEventArgs e)
{
ModAnimation.AniStart(ModAnimation.AaOpacity(PanButtons, 1d - PanButtons.Opacity, 120),
"PageLoginProfileSkin Button");
}
private void HidePanel(object sender, EventArgs e)
{
if (BtnEdit.ContextMenu.IsOpen || BtnSkin.ContextMenu.IsOpen || PanData.IsMouseOver)
return;
ModAnimation.AniStart(ModAnimation.AaOpacity(PanButtons, -PanButtons.Opacity, 120),
"PageLoginProfileSkin Button");
}
private void MenuAccountOptions_Closed(object sender, RoutedEventArgs e)
{
HidePanel(sender, e);
}
// 皮肤与披风子菜单
private void BtnSkin_Click(object sender, EventArgs e)
{
BtnSkin.ContextMenu.IsOpen = true;
}
// 账号信息子菜单
private void BtnEdit_Click(object sender, EventArgs e)
{
BtnEdit.ContextMenu.IsOpen = true;
}
// 修改密码
private void BtnEditPassword_Click(object sender, RoutedEventArgs e)
{
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Ms)
{
ModBase.OpenWebsite("https://account.live.com/password/Change");
}
else if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Auth)
{
var server = ModProfile.selectedProfile.Server;
ModBase.OpenWebsite(server.Replace("/api/yggdrasil/authserver" + (server.EndsWithF("/") ? "/" : ""),
"/user/profile"));
}
else
{
HintService.Hint(Lang.Text("Launch.Account.ProfileSkin.PasswordUnsupported"));
}
}
// 修改 ID
private void BtnEditName_Click(object sender, RoutedEventArgs e)
{
ModProfile.EditProfileId();
}
// 选择档案
private void ChangeProfile(object sender, EventArgs e)
{
ModProfile.selectedProfile = null;
ModBase.RunInUi(() =>
{
ModMain.frmLaunchLeft.RefreshPage(true);
ModMain.frmLaunchLeft.BtnLaunch.IsEnabled = false;
});
}
// 修改皮肤
private void Skin_Click(object sender, RoutedEventArgs e)
{
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Ms)
ModProfile.ChangeSkinMs();
else if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Auth)
ModBase.OpenWebsite(ModProfile.selectedProfile.Server.BeforeFirst("api/yggdrasil/authserver") +
"user/closet");
else
HintService.Hint(Lang.Text("Launch.Account.ProfileSkin.SkinUnsupported"));
}
// 保存皮肤
private void BtnSkinSave_Click(object sender, RoutedEventArgs e)
{
Skin.BtnSkinSave_Click(sender, e);
}
// 刷新皮肤
private void BtnSkinRefresh_Click(object sender, RoutedEventArgs e)
{
Skin.RefreshClick(sender, e);
}
// 修改披风
private void BtnSkinCape_Click(object sender, RoutedEventArgs e)
{
if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Ms)
Skin.BtnSkinCape_Click(sender, e);
else if (ModProfile.selectedProfile.Type == ModLaunch.McLoginType.Auth)
ModBase.OpenWebsite(ModProfile.selectedProfile.Server.BeforeFirst("api/yggdrasil/authserver") +
"user/closet");
else
HintService.Hint(Lang.Text("Launch.Account.ProfileSkin.CapeUnsupported"));
}
#endregion
}