Files
MRCC/launcher/PCL-CE/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs
T

3782 lines
158 KiB
C#
Raw Normal View History

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Windows;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.Minecraft;
using PCL.Core.Minecraft.Launch.Utils;
using PCL.Core.Utils;
using PCL.Core.Utils.OS;
using PCL.Core.Utils.Secret;
using PCL.Network;
using PCL.Core.IO.Net.Http;
using PCL.Core.Minecraft.IdentityModel.Yggdrasil;
using System.Globalization;
namespace PCL;
public static class ModLaunch
{
public const string mesaLoaderWindowsVersion = "26.0.4";
#region 预检测
private static void McLaunchPrecheck()
{
if (Config.Debug.AddRandomDelay)
Thread.Sleep(RandomUtils.NextInt(100, 2000));
// 检查路径
if (ModInstanceList.McMcInstanceSelected.PathIndie.Contains("!") ||
ModInstanceList.McMcInstanceSelected.PathIndie.Contains(";"))
throw new Exception(Lang.Text("Minecraft.Launch.Precheck.InvalidPathChars", ModInstanceList.McMcInstanceSelected.PathIndie));
if (ModInstanceList.McMcInstanceSelected.PathInstance.Contains("!") ||
ModInstanceList.McMcInstanceSelected.PathInstance.Contains(";"))
throw new Exception(Lang.Text("Minecraft.Launch.Precheck.InvalidPathChars", ModInstanceList.McMcInstanceSelected.PathInstance));
if (ModBase.IsUtf8CodePage() && !States.Hint.NonAsciiGamePath &&
!ModInstanceList.McMcInstanceSelected.PathInstance.IsASCII())
{
var userChoice = ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Precheck.NonAsciiPath.Message", ModInstanceList.McMcInstanceSelected.Name),
Lang.Text("Minecraft.Launch.Precheck.NonAsciiPath.Title"), Lang.Text("Minecraft.Launch.Precheck.NonAsciiPath.Continue"), Lang.Text("Minecraft.Launch.Precheck.NonAsciiPath.Back"), Lang.Text("Common.Hint.DoNotShowAgain"));
if (userChoice == 2) throw new Exception("$$");
if (userChoice == 3) States.Hint.NonAsciiGamePath = true;
}
// 检查实例
if (ModInstanceList.McMcInstanceSelected is null)
throw new Exception(Lang.Text("Minecraft.Launch.Precheck.NoInstance"));
ModInstanceList.McMcInstanceSelected.Load();
if (ModInstanceList.McMcInstanceSelected.state == McInstanceState.Error)
throw new Exception(Lang.Text("Minecraft.Launch.Precheck.InstanceError", ModInstanceList.McMcInstanceSelected.Desc));
// 检查输入信息
var checkResult = "";
ModBase.RunInUiWait(() => checkResult = ModProfile.IsProfileValid());
if (ModProfile.selectedProfile is null) // 没选档案
{
checkResult = Lang.Text("Minecraft.Launch.Precheck.NoProfile");
}
else if (ModInstanceList.McMcInstanceSelected.Info.HasLabyMod ||
Config.InstanceAuth.LoginRequirementSolution[ModInstanceList.McMcInstanceSelected?.PathInstance] == 1) // 要求正版验证
{
if (ModProfile.selectedProfile.Type != McLoginType.Ms) checkResult = Lang.Text("Minecraft.Launch.Precheck.RequireMicrosoft");
}
else if (Config.InstanceAuth.LoginRequirementSolution[ModInstanceList.McMcInstanceSelected?.PathInstance] == 2) // 要求第三方验证
{
if (ModProfile.selectedProfile.Type != McLoginType.Auth)
checkResult = Lang.Text("Minecraft.Launch.Precheck.RequireThirdParty");
else if (ModProfile.selectedProfile.Server.BeforeLast("/authserver") !=
Config.InstanceAuth.AuthServerAddress[ModInstanceList.McMcInstanceSelected?.PathInstance])
checkResult = Lang.Text("Minecraft.Launch.Precheck.AuthServerMismatch");
}
else if (Config.InstanceAuth.LoginRequirementSolution[ModInstanceList.McMcInstanceSelected?.PathInstance] == 3) // 要求正版验证或第三方验证
{
if (ModProfile.selectedProfile.Type == McLoginType.Legacy)
checkResult = Lang.Text("Minecraft.Launch.Precheck.RequireMicrosoftOrThirdParty");
else if (ModProfile.selectedProfile.Type == McLoginType.Auth &&
ModProfile.selectedProfile.Server.BeforeLast("/authserver") !=
Config.InstanceAuth.AuthServerAddress[ModInstanceList.McMcInstanceSelected?.PathInstance])
checkResult = Lang.Text("Minecraft.Launch.Precheck.AuthServerMismatch");
}
if (!string.IsNullOrEmpty(checkResult))
throw new ArgumentException(checkResult);
#if BETA
if (currentLaunchOptions?.SaveBatch is null) // 保存脚本时不提示
{
ModBase.RunInNewThread(() =>
{
switch (States.System.LaunchCount)
{
case 10:
case 20:
case 40:
case 60:
case 80:
case 100:
case 120:
case 150:
case 200:
case 250:
case 300:
case 350:
case 400:
case 500:
case 600:
case 700:
case 800:
case 900:
case 1000:
case 1200:
case 1400:
case 1600:
case 1800:
case 2000:
if (ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Donate.Message", States.System.LaunchCount),
Lang.Text("Minecraft.Launch.Donate.Title", States.System.LaunchCount),
Lang.Text("Minecraft.Launch.Donate.Support"),
Lang.Text("Minecraft.Launch.Donate.Decline")) == 1)
{
ModBase.OpenWebsite("https://afdian.com/a/LTCat");
}
break;
}
}, "Donate");
}
#endif
#if DEBUG || DEBUGCI
return;
#endif
// 正版购买提示
if (!ModProfile.profileList.Any(x => x.Type == McLoginType.Ms))
{
if (Lang.IsFeaturesUnrestricted)
{
if (ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.PurchaseHint.Message"),
Lang.Text("Minecraft.Launch.PurchaseHint.Title"), Lang.Text("Minecraft.Launch.PurchaseHint.Purchase"), Lang.Text("Minecraft.Launch.PurchaseHint.Later")) ==
1)
ModBase.OpenWebsite(
"https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj");
}
else
{
switch (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.AccountVerification.Message"),
Lang.Text("Minecraft.Launch.AccountVerification.Title"),
Lang.Text("Minecraft.Launch.AccountVerification.Purchase"),
Lang.Text("Minecraft.Launch.AccountVerification.Demo"),
Lang.Text("Minecraft.Launch.AccountVerification.Back"),
button1Action: () =>
ModBase.OpenWebsite(
"https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj")))
{
case 2:
{
HintService.Hint(Lang.Text("Minecraft.Launch.DemoMode"), HintType.Error);
currentLaunchOptions.ExtraArgs.Add("--demo");
break;
}
case 3:
{
throw new Exception("$$");
}
}
}
}
}
#endregion
#region 开始
public static bool isLaunching;
public static McLaunchOptions currentLaunchOptions;
public class McLaunchOptions
{
/// <summary>
/// 额外的启动参数。
/// </summary>
public List<string> ExtraArgs = new();
/// <summary>
/// 强行指定启动的 MC 实例。
/// 默认值:Nothing。使用 McInstanceCurrent。
/// </summary>
public McInstance instance = null;
/// <summary>
/// 是否为 “测试游戏” 按钮启动的游戏。
/// 如果是,则显示游戏实时日志。
/// </summary>
public bool IsTest = false;
/// <summary>
/// 将启动脚本保存到该地址,然后取消启动。这同时会改变启动时的提示等。
/// 默认值:Nothing。不保存。
/// </summary>
public string SaveBatch = null;
/// <summary>
/// 强制指定在启动后进入的服务器 IP。
/// 默认值:Nothing。使用实例设置的值。
/// </summary>
public string ServerIp = null;
/// <summary>
/// 指定在启动之后进入的存档名称。
/// 默认值:Nothing。使用实例设置的值。
/// </summary>
public string WorldName = null;
}
/// <summary>
/// 尝试启动 Minecraft。必须在 UI 线程调用。
/// 返回是否实际开始了启动(如果没有,则一定弹出了错误提示)。
/// </summary>
public static bool McLaunchStart(McLaunchOptions options = null)
{
isLaunching = true;
currentLaunchOptions = options ?? new McLaunchOptions();
// 预检查
if (!ModBase.RunInUi())
throw new Exception("McLaunchStart 必须在 UI 线程调用!");
if (mcLaunchLoader.State == ModBase.LoadState.Loading)
{
HintService.Hint(Lang.Text("Minecraft.Launch.Error.AlreadyLaunching"), HintType.Error);
isLaunching = false;
return false;
}
// 强制切换需要启动的实例
if (currentLaunchOptions.instance is not null &&
ModInstanceList.McMcInstanceSelected != currentLaunchOptions.instance)
{
McLaunchLog("在启动前切换到实例 " + currentLaunchOptions.instance.Name);
// 检查实例
currentLaunchOptions.instance.Load();
if (currentLaunchOptions.instance.state == McInstanceState.Error)
{
HintService.Hint(Lang.Text("Minecraft.Launch.Error.CannotLaunch", currentLaunchOptions.instance.Desc), HintType.Error);
isLaunching = false;
return false;
}
// 切换实例
ModInstanceList.McMcInstanceSelected = currentLaunchOptions.instance;
States.Game.SelectedInstance = ModInstanceList.McMcInstanceSelected.Name;
ModMain.frmLaunchLeft.RefreshButtonsUI();
ModMain.frmLaunchLeft.RefreshPage(false);
}
ModMain.frmMain.AprilGiveup();
// 禁止进入实例选择页面(否则就可以在启动中切换 McInstanceCurrent 了)
ModMain.frmMain.pageStack =
ModMain.frmMain.pageStack.Where(p => p.page != FormMain.PageType.InstanceSelect).ToList();
// 实际启动加载器
mcLaunchLoader.Start(options, true);
return true;
}
/// <summary>
/// 记录启动日志。
/// </summary>
public static void McLaunchLog(string text)
{
text = McLogFilter.FilterUserName(McLogFilter.FilterAccessToken(text, '*'), '*');
ModBase.RunInUi(() =>
ModMain.frmLaunchRight.LabLog.Text += "\r\n" + "[" + TimeUtils.GetTimeNow() + "] " + text);
ModBase.Log("[Launch] " + text);
}
// 启动状态切换
public static ModLoader.LoaderTask<McLaunchOptions, object> mcLaunchLoader = new("Loader Launch", McLaunchStart)
{ OnStateChanged = a => McLaunchState((dynamic)a) };
public static ModLoader.LoaderCombo<object> mcLaunchLoaderReal;
public static Process mcLaunchProcess;
public static ModWatcher.Watcher mcLaunchWatcher;
private static void McLaunchState(ModLoader.LoaderTask<McLaunchOptions, object> loader)
{
switch (mcLaunchLoader.State)
{
case ModBase.LoadState.Finished:
case ModBase.LoadState.Failed:
case ModBase.LoadState.Waiting:
case ModBase.LoadState.Aborted:
{
ModMain.frmLaunchLeft.PageChangeToLogin();
break;
}
case ModBase.LoadState.Loading:
{
// 在预检测结束后再触发动画
ModMain.frmLaunchRight.LabLog.Text = "";
break;
}
}
}
/// <summary>
/// 指定启动中断时的提示文本。若不为 Nothing 则会显示为绿色。
/// </summary>
private static string abortHint;
// 实际的启动方法
private static void McLaunchStart(ModLoader.LoaderTask<McLaunchOptions, object> loader)
{
// 开始动画
ModBase.RunInUiWait(ModMain.frmLaunchLeft.PageChangeToLaunching);
// 预检测(预检测的错误将直接抛出)
try
{
McLaunchPrecheck();
McLaunchLog("预检测已通过");
}
catch (Exception ex)
{
if (!ex.Message.StartsWithF("$$"))
HintService.Hint(Lang.Text("Minecraft.Launch.Precheck.Failed.WithDetail", ex.Message), HintType.Error);
throw;
}
// 正式加载
try
{
// 构造主加载器
var loaders = new List<ModLoader.LoaderBase>
{
new ModLoader.LoaderTask<int, int>(Lang.Text("Minecraft.Launch.Stage.GetJava"), McLaunchJava) { ProgressWeight = 4d, block = false },
mcLoginLoader,
new ModLoader.LoaderCombo<string>(Lang.Text("Minecraft.Launch.Stage.CompleteFiles"),
ModDownload.DlClientFix(ModInstanceList.McMcInstanceSelected, false,
ModDownload.AssetsIndexExistsBehaviour.DownloadInBackground))
{ ProgressWeight = 15d, show = false },
new ModLoader.LoaderTask<string, List<ModLibrary.McLibToken>>(Lang.Text("Minecraft.Launch.Stage.GetArguments"), McLaunchArgumentMain)
{ ProgressWeight = 2d },
new ModLoader.LoaderTask<List<ModLibrary.McLibToken>, int>(Lang.Text("Minecraft.Launch.Stage.ExtractNatives"), McLaunchNatives)
{ ProgressWeight = 2d },
new ModLoader.LoaderTask<int, int>(Lang.Text("Minecraft.Launch.Stage.PreLaunch"), _ => McLaunchPrerun()) { ProgressWeight = 1d },
new ModLoader.LoaderTask<int, int>(Lang.Text("Minecraft.Launch.Stage.CustomCommand"), McLaunchCustom) { ProgressWeight = 1d },
new ModLoader.LoaderTask<int, Process>(Lang.Text("Minecraft.Launch.Stage.StartProcess"), McLaunchRun) { ProgressWeight = 2d },
new ModLoader.LoaderTask<Process, int>(Lang.Text("Minecraft.Launch.Stage.WaitWindow"), McLaunchWait) { ProgressWeight = 1d },
new ModLoader.LoaderTask<int, int>(Lang.Text("Minecraft.Launch.Stage.End"), _ => McLaunchEnd()) { ProgressWeight = 1d }
}; // .ProgressWeight = 15, .Block = False
var launchLoader = new ModLoader.LoaderCombo<object>(Lang.Text("Minecraft.Launch.Stage.Root"), loaders) { show = false };
if (mcLoginLoader.State == ModBase.LoadState.Finished)
mcLoginLoader.State = ModBase.LoadState.Waiting; // 要求重启登录主加载器,它会自行决定是否启动副加载器
// 等待加载器执行并更新 UI
mcLaunchLoaderReal = launchLoader;
abortHint = null;
launchLoader.Start();
// 任务栏进度条
ModLoader.LoaderTaskbarAdd(launchLoader);
while (launchLoader.State == ModBase.LoadState.Loading)
{
ModMain.frmLaunchLeft.Dispatcher.Invoke(ModMain.frmLaunchLeft.LaunchingRefresh);
Thread.Sleep(100);
}
ModMain.frmLaunchLeft.Dispatcher.Invoke(ModMain.frmLaunchLeft.LaunchingRefresh);
// 成功与失败处理
switch (launchLoader.State)
{
case ModBase.LoadState.Finished:
{
HintService.Hint(Lang.Text("Minecraft.Launch.Success", ModInstanceList.McMcInstanceSelected.Name), HintType.Success);
break;
}
case ModBase.LoadState.Aborted:
{
if (abortHint is null)
HintService.Hint(currentLaunchOptions?.SaveBatch is null ? Lang.Text("Minecraft.Launch.Cancelled") : Lang.Text("Minecraft.Launch.ExportScript.Cancelled"));
else
HintService.Hint(abortHint, HintType.Success);
break;
}
case ModBase.LoadState.Failed:
{
throw launchLoader.Error;
}
default:
{
throw new Exception(Lang.Text("Minecraft.Launch.Error.InvalidState", ModBase.GetStringFromEnum(launchLoader.State)));
}
}
isLaunching = false;
}
catch (Exception ex)
{
var currentEx = ex;
while (currentEx is not null)
{
if (currentEx.Message.StartsWithF("$"))
{
// 若有以 $ 开头的错误信息,则以此为准显示提示
// 若错误信息为 $$,则不提示
if (currentEx.Message != "$$")
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Error.SpecialMessage.WithDetail",
currentEx.Message.TrimStart('$')),
currentLaunchOptions?.SaveBatch is null
? Lang.Text("Launch.Error.Title")
: Lang.Text("Launch.Error.ExportScriptTitle"));
throw;
}
if (currentEx.InnerException is null)
break;
// 检查下一级错误
currentEx = currentEx.InnerException;
}
// 没有特殊处理过的错误信息
McLaunchLog("错误:" + ex);
ModBase.Log(
ex,
currentLaunchOptions?.SaveBatch is null
? "Minecraft launch failed"
: "Export script failed",
ModBase.LogLevel.Msgbox,
currentLaunchOptions?.SaveBatch is null
? Lang.Text("Launch.Error.Title")
: Lang.Text("Launch.Error.ExportScriptTitle"),
userSummary: currentLaunchOptions?.SaveBatch is null
? Lang.Text("Minecraft.Launch.Error.LaunchFailed")
: Lang.Text("Minecraft.Launch.Error.ExportScriptFailed"));
throw;
}
}
#endregion
#region 档案验证
#region 主模块
// 登录方式
public enum McLoginType
{
Legacy = 1,
Auth = 2,
Ms = 3
}
// 各个登录方式的对应数据
public abstract class McLoginData
{
/// <summary>
/// 登录方式。
/// </summary>
public McLoginType LoginType;
public override bool Equals(object obj)
{
return obj is not null && obj.GetHashCode() == GetHashCode();
}
}
#region 第三方验证类型
public class McLoginServer : McLoginData
{
/// <summary>
/// 登录服务器基础地址。
/// </summary>
public string BaseUrl;
/// <summary>
/// 登录方式的描述字符串,如 “正版”、“统一通行证”。
/// </summary>
public string Description;
/// <summary>
/// 是否在本次登录中强制要求玩家重新选择角色,目前仅对 Authlib-Injector 生效。
/// </summary>
public bool ForceReselectProfile = false;
/// <summary>
/// 是否已经存在该验证信息,用于判断是否为新增档案。
/// </summary>
public bool IsExist = false;
/// <summary>
/// 登录密码。
/// </summary>
public string Password;
/// <summary>
/// 登录用户名。
/// </summary>
public string UserName;
public McLoginServer(McLoginType type)
{
this.LoginType = type;
}
public override int GetHashCode()
{
return (int)Math.Round(ModBase.GetHash(UserName + Password + BaseUrl + (int)LoginType) %
(decimal)int.MaxValue);
}
}
#endregion
#region 正版验证类型
public class McLoginMs : McLoginData
{
public string AccessToken = "";
/// <summary>
/// 缓存的 OAuth RefreshToken。若没有则为空字符串。
/// </summary>
public string OAuthRefreshToken = "";
public string ProfileJson = "";
public string UserName = "";
public string Uuid = "";
public McLoginMs()
{
LoginType = McLoginType.Ms;
}
public override int GetHashCode()
{
return (int)Math.Round(ModBase.GetHash(OAuthRefreshToken + AccessToken + Uuid + UserName + ProfileJson) %
(decimal)int.MaxValue);
}
}
#endregion
#region 离线验证类型
public class McLoginLegacy : McLoginData
{
/// <summary>
/// 若采用正版皮肤,则为该皮肤名。
/// </summary>
public string SkinName;
/// <summary>
/// 皮肤种类。
/// </summary>
public int SkinType;
/// <summary>
/// 登录用户名。
/// </summary>
public string UserName;
/// <summary>
/// UUID。
/// </summary>
public string Uuid;
public McLoginLegacy()
{
LoginType = McLoginType.Legacy;
}
public override int GetHashCode()
{
return (int)Math.Round(
ModBase.GetHash(UserName + SkinType + SkinName + (int)LoginType) % (decimal)int.MaxValue);
}
}
#endregion
// 登录返回结果
public struct McLoginResult
{
public string Name;
public string Uuid;
public string AccessToken;
public string Type;
public string ClientToken;
/// <summary>
/// 进行微软登录时返回的 profile 信息。
/// </summary>
public string ProfileJson;
}
// 登录主模块加载器
public static ModLoader.LoaderTask<McLoginData, McLoginResult> mcLoginLoader =
new(Lang.Text("Minecraft.Launch.Stage.Login"), McLoginStart, McLoginInput, ThreadPriority.BelowNormal)
{ reloadTimeout = 1, ProgressWeight = 15d, block = false };
public static McLoginData McLoginInput()
{
McLoginData loginData = null;
try
{
loginData = ModProfile.GetLoginData();
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Minecraft.Launch.Login.Error.Input"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Minecraft.Launch.Login.Error.Input"));
}
return loginData;
}
private static void McLoginStart(ModLoader.LoaderTask<McLoginData, McLoginResult> data)
{
ModBase.Log("[Profile] 开始加载选定档案");
// 校验登录信息
var checkResult = ModProfile.IsProfileValid();
if (!string.IsNullOrEmpty(checkResult))
throw new ArgumentException(checkResult);
// 获取对应加载器
ModLoader.LoaderBase loader = null;
switch (data.input.LoginType)
{
case McLoginType.Ms:
{
loader = mcLoginMsLoader;
break;
}
case McLoginType.Legacy:
{
loader = mcLoginLegacyLoader;
break;
}
case McLoginType.Auth:
{
loader = mcLoginAuthLoader;
break;
}
}
// 尝试加载
loader.WaitForExit(data.input, mcLoginLoader, data.isForceRestarting);
data.output = (McLoginResult)((dynamic)loader).output;
ModBase.RunInUi(() => ModMain.frmLaunchLeft.RefreshPage(false)); // 刷新自动填充列表
ModBase.Log("[Profile] 选定档案加载完成");
}
#endregion
// 各个登录方式的主对象与输入构造
public static ModLoader.LoaderTask<McLoginMs, McLoginResult> mcLoginMsLoader =
new("Loader Login Ms", McLoginMsStart) { reloadTimeout = 1 };
public static ModLoader.LoaderTask<McLoginLegacy, McLoginResult> mcLoginLegacyLoader =
new("Loader Login Legacy", McLoginLegacyStart);
public static ModLoader.LoaderTask<McLoginServer, McLoginResult> mcLoginAuthLoader =
new("Loader Login Auth", McLoginServerStart) { reloadTimeout = 1000 * 60 * 10 };
// 主加载函数,返回所有需要的登录信息
private static long mcLoginMsRefreshTime; // 上次刷新登录的时间
#region 正版验证
private static void McLoginMsStart(ModLoader.LoaderTask<McLoginMs, McLoginResult> data)
{
var input = data.input;
var logUsername = input.UserName;
var isNewProfile = true;
ModProfile.ProfileLog($"验证方式:正版({(string.IsNullOrEmpty(logUsername) ? "尚未登录" : logUsername)}");
data.Progress = 0.05d;
// 已登录且不需要强制重启且登录未过期
if (!data.isForceRestarting && !string.IsNullOrEmpty(input.AccessToken) &&
mcLoginMsRefreshTime > 0L &&
TimeUtils.GetTimeTick() - mcLoginMsRefreshTime < 1000 * 60 * 10)
{
data.output = new McLoginResult
{
AccessToken = input.AccessToken,
Name = input.UserName,
Uuid = input.Uuid,
Type = "Microsoft",
ClientToken = input.Uuid,
ProfileJson = input.ProfileJson
};
mcLoginMsRefreshTime = TimeUtils.GetTimeTick();
ModProfile.ProfileLog("正版验证完成");
return;
}
data.Progress = 0.1d;
// 尝试获取 OAuthToken
var oauthTokens = GetOAuthTokens(data, input, out var skipAuth);
if (skipAuth)
{
data.Progress = 0.99d;
var profile = ModProfile.selectedProfile;
data.output = new McLoginResult
{
AccessToken = profile.AccessToken,
Name = profile.Username,
Uuid = profile.Uuid,
Type = "Microsoft"
};
return;
}
var oauthAccessToken = oauthTokens[0];
var oauthRefreshToken = oauthTokens[1];
ThrowIfAborted(data);
data.Progress = 0.25d;
// Step 2: XBL Token
var xblToken = MsLoginStep2(oauthAccessToken);
if (string.IsNullOrEmpty(xblToken) || xblToken == "Ignore")
goto SkipLogin;
data.Progress = 0.4d;
ThrowIfAborted(data);
// Step 3: XSTS / Minecraft login
var tokens = MsLoginStep3(xblToken);
if (tokens.Length < 2 || tokens[1] == "Ignore")
goto SkipLogin;
data.Progress = 0.55d;
ThrowIfAborted(data);
// Step 4: Final access token
var accessToken = MsLoginStep4(tokens);
if (string.IsNullOrEmpty(accessToken) || accessToken == "Ignore")
goto SkipLogin;
data.Progress = 0.7d;
ThrowIfAborted(data);
// Step 5: Additional setup
MsLoginStep5(accessToken);
data.Progress = 0.85d;
ThrowIfAborted(data);
// Step 6: Profile info
var result = MsLoginStep6(accessToken);
if (result.Length < 3 || result[2] == "Ignore")
goto SkipLogin;
data.Progress = 0.98d;
// 检查是否已有相同档案
foreach (var profile in ModProfile.profileList)
if (profile.Type == McLoginType.Ms &&
string.Equals(profile.Username, result[1], StringComparison.Ordinal) &&
string.Equals(profile.Uuid, result[0], StringComparison.Ordinal))
{
isNewProfile = false;
if (ModProfile.isCreatingProfile)
{
var index = ModProfile.profileList.IndexOf(profile);
ModProfile.profileList[index].Username = result[1];
ModProfile.profileList[index].AccessToken = accessToken;
ModProfile.profileList[index].RefreshToken = oauthRefreshToken;
HintService.Hint(Lang.Text("Minecraft.Launch.Login.Microsoft.ProfileAlreadyAdded"));
goto SkipLogin;
}
}
// 输出登录结果
if (isNewProfile)
{
var newProfile = new ModProfile.McProfile
{
Type = McLoginType.Ms,
Uuid = result[0],
Username = result[1],
AccessToken = accessToken,
RefreshToken = oauthRefreshToken,
Expires = 1743779140286L,
Desc = "",
RawJson = result[2]
};
ModProfile.profileList.Add(newProfile);
ModProfile.selectedProfile = newProfile;
ModProfile.isCreatingProfile = false;
}
else
{
var index = ModProfile.profileList.IndexOf(ModProfile.selectedProfile);
ModProfile.profileList[index].Username = result[1];
ModProfile.profileList[index].AccessToken = accessToken;
ModProfile.profileList[index].RefreshToken = oauthRefreshToken;
}
ModProfile.SaveProfile();
data.output = new McLoginResult
{
AccessToken = accessToken,
Name = result[1],
Uuid = result[0],
Type = "Microsoft",
ClientToken = result[0],
ProfileJson = result[2]
};
SkipLogin:
mcLoginMsRefreshTime = TimeUtils.GetTimeTick();
ModProfile.ProfileLog("正版验证完成");
}
/// <summary>
/// 获取 OAuth Tokens,处理刷新和重新登录逻辑
/// </summary>
private static string[] GetOAuthTokens(ModLoader.LoaderTask<McLoginMs, McLoginResult> data, McLoginMs input,
out bool skipAuth)
{
skipAuth = false;
string[] tokens;
while (true)
{
if (string.IsNullOrEmpty(input.OAuthRefreshToken))
{
tokens = MsLoginStep1New(data);
}
else
{
tokens = MsLoginStep1Refresh(input.OAuthRefreshToken);
if (tokens.Length > 0 && tokens[0] == "Relogin")
{
// 刷新令牌已失效,清除后回退到设备代码流重新登录,避免无限循环
input.OAuthRefreshToken = "";
continue;
}
}
if (tokens.Length > 0 && tokens[0] == "Ignore")
{
skipAuth = true;
return tokens;
}
return tokens;
}
}
/// <summary>
/// 检查是否被中断
/// </summary>
private static void ThrowIfAborted(ModLoader.LoaderTask<McLoginMs, McLoginResult> data)
{
if (data.IsAborted)
throw new ThreadInterruptedException();
}
/// <summary>
/// 正版验证步骤 1:通过设备代码流获取账号信息
/// </summary>
/// <returns>OAuth 验证完成的返回结果</returns>
private static string[] MsLoginStep1New(ModLoader.LoaderTask<McLoginMs, McLoginResult> data)
{
// 参考:https://learn.microsoft.com/zh-cn/entra/identity-platform/v2-oauth2-device-code
// 初始请求
Retry: ;
McLaunchLog("开始正版验证 Step 1/6(原始登录)");
JsonObject prepareJson;
var parameters = new Dictionary<string, string>
{
{ "client_id", Secrets.MSOAuthClientId },
{ "tenant", "/consumers" },
{ "scope", "XboxLive.signin offline_access" }
};
using (var response = HttpRequest
.CreatePost("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
.WithFormContent(parameters)
.SendAsync()
.GetAwaiter()
.GetResult())
{
response.EnsureSuccessStatusCode();
prepareJson = (JsonObject)ModBase.GetJson(response.AsString());
}
McLaunchLog("网页登录地址:" + prepareJson["verification_uri"]);
// 弹窗
var converter = new ModMain.MyMsgBoxConverter
{ Content = prepareJson, ForceWait = true, Type = ModMain.MyMsgBoxType.Login };
ModMain.WaitingMyMsgBox.Add(converter);
while (converter.Result is null)
Thread.Sleep(100);
if (converter.Result is ModBase.RestartException)
{
if (ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.PasswordRequired.Message", ModBase.vbLQ, ModBase.vbRQ),
Lang.Text("Minecraft.Launch.Login.PasswordRequired.Title"), Lang.Text("Minecraft.Launch.Login.PasswordRequired.Relogin"), Lang.Text("Minecraft.Launch.Login.PasswordRequired.SetPassword"), Lang.Text("Common.Action.Cancel"),
button2Action: () => ModBase.OpenWebsite("https://account.live.com/password/Change")) ==
1) goto Retry;
throw new Exception("$$");
}
if (converter.Result is Exception) throw (Exception)converter.Result;
return (string[])converter.Result;
}
/// <summary>
/// 正版验证步骤 1,刷新登录:从 OAuth Code 或 OAuth RefreshToken 获取 {OAuth accessToken, OAuth RefreshToken}
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
private static string[] MsLoginStep1Refresh(string code)
{
McLaunchLog("开始正版验证 Step 1/6(刷新登录)");
if (string.IsNullOrEmpty(code))
throw new ArgumentException("传入的 Code 为空", nameof(code));
string result = null;
try
{
var parameters = new Dictionary<string, string>
{
{ "client_id", Secrets.MSOAuthClientId },
{ "refresh_token", code },
{ "grant_type", "refresh_token" },
{ "scope", "XboxLive.signin offline_access" }
};
using (var response = HttpRequest
.CreatePost("https://login.live.com/oauth20_token.srf")
.WithFormContent(parameters)
.SendAsync()
.GetAwaiter()
.GetResult())
{
result = response.AsString();
if (!response.IsSuccess)
throw new HttpRequestException(
$"刷新登录请求失败,状态码 {(int)response.StatusCode}{result}");
}
}
catch (ThreadInterruptedException ex)
{
ModBase.Log(ex, "加载线程已终止");
}
catch (Exception ex)
{
if (ex.Message.ContainsF("invalid_grant", true) || ex.Message.ContainsF("must sign in again", true) ||
ex.Message.ContainsF("must first sign in", true) || ex.Message.ContainsF("password expired", true) ||
(ex.Message.Contains("refresh_token") && ex.Message.Contains("is not valid"))) // #269
return new[] { "Relogin", "" };
ModProfile.ProfileLog("正版验证 Step 1/6 获取 OAuth Token 失败:" + ex);
var isIgnore = false;
ModBase.RunInUiWait(() =>
{
if (!isLaunching)
return;
if (ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
isIgnore = true;
});
if (isIgnore) return new[] { "Ignore", "" };
// 用户取消或登录线程已结束,静默中止启动,避免落入下方的 JSON 解析空引用
throw new Exception("$$");
}
var resultJson = (JsonObject)ModBase.GetJson(result);
var accessToken = resultJson["access_token"].ToString();
var refreshToken = resultJson["refresh_token"].ToString();
return new[] { accessToken, refreshToken };
}
private class XBLTokenRequestData
{
public PropertiesData Properties { get; set; }
public string RelyingParty { get; set; }
public string TokenType { get; set; }
public class PropertiesData
{
public string AuthMethod { get; set; }
public string SiteName { get; set; }
public string RpsTicket { get; set; }
}
}
/// <summary>
/// 正版验证步骤 2:从 OAuth accessToken 获取 XBLToken
/// </summary>
/// <param name="accessToken">OAuth accessToken</param>
/// <returns>XBLToken</returns>
private static string MsLoginStep2(string accessToken)
{
ModProfile.ProfileLog("开始正版验证 Step 2/6: 获取 XBLToken");
if (string.IsNullOrEmpty(accessToken))
throw new ArgumentException("传入的 AccessToken 为空", nameof(accessToken));
var requestData = new XBLTokenRequestData
{
Properties = new XBLTokenRequestData.PropertiesData
{
AuthMethod = "RPS",
SiteName = "user.auth.xboxlive.com",
RpsTicket = $"d={accessToken}"
},
RelyingParty = "http://auth.xboxlive.com",
TokenType = "JWT"
};
string result = null;
try
{
using (var response = HttpRequest
.CreatePost("https://user.auth.xboxlive.com/user/authenticate")
.WithJsonContent(requestData)
.SendAsync()
.GetAwaiter()
.GetResult())
{
response.EnsureSuccessStatusCode();
result = response.AsString();
}
}
catch (Exception ex)
{
ModProfile.ProfileLog("正版验证 Step 2/6 获取 XBLToken 失败:" + ex);
var isIgnore = false;
ModBase.RunInUiWait(() =>
{
if (!isLaunching)
return;
if (ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
isIgnore = true;
});
if (isIgnore) return "Ignore";
}
var resultJson = (JsonObject)ModBase.GetJson(result);
var xBLToken = resultJson["Token"].ToString();
return xBLToken;
}
private class XSTSTokenRequestData
{
public PropertiesData Properties { get; set; }
public string RelyingParty { get; set; }
public string TokenType { get; set; }
public class PropertiesData
{
public string SandboxId { get; set; }
public List<string> UserTokens { get; set; }
}
}
/// <summary>
/// 正版验证步骤 3:从 XBLToken 获取 {XSTSToken, UHS}
/// </summary>
/// <returns>包含 XSTSToken 与 UHS 的字符串组</returns>
private static string[] MsLoginStep3(string xBLToken)
{
ModProfile.ProfileLog("开始正版验证 Step 3/6: 获取 XSTSToken");
if (string.IsNullOrEmpty(xBLToken))
throw new ArgumentException("XBLToken 为空,无法获取数据", nameof(xBLToken));
var requestData = new XSTSTokenRequestData
{
Properties = new XSTSTokenRequestData.PropertiesData
{
SandboxId = "RETAIL",
UserTokens = new[] { xBLToken }.ToList()
},
RelyingParty = "rp://api.minecraftservices.com/",
TokenType = "JWT"
};
string result;
using (var response = HttpRequest
.CreatePost("https://xsts.auth.xboxlive.com/xsts/authorize")
.WithJsonContent(requestData)
.SendAsync()
.GetAwaiter()
.GetResult())
{
result = response.AsString();
if (!response.IsSuccess)
{
// 参考 https://github.com/PrismarineJS/prismarine-auth/blob/master/src/common/Constants.js
if (result.Contains("2148916227"))
{
ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.Banned"), Lang.Text("Minecraft.Launch.Login.Failed"), Lang.Text("Minecraft.Launch.Login.IKnow"), isWarn: true);
throw new Exception("$$");
}
if (result.Contains("2148916233"))
{
if (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.XboxNotRegistered"), Lang.Text("Minecraft.Launch.Login.Hint"), Lang.Text("Minecraft.Launch.Login.Register"), Lang.Text("Common.Action.Cancel")) == 1)
ModBase.OpenWebsite("https://signup.live.com/signup");
throw new Exception("$$");
}
if (result.Contains("2148916235"))
{
ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.RegionBlocked"), Lang.Text("Minecraft.Launch.Login.Failed"), Lang.Text("Minecraft.Launch.Login.IKnow"));
throw new Exception("$$");
}
if (result.Contains("2148916238"))
{
if (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.Underage.Message"),
Lang.Text("Minecraft.Launch.Login.Hint"), Lang.Text("Minecraft.Launch.Login.Microsoft.Underage.AgeOver13"), Lang.Text("Minecraft.Launch.Login.Microsoft.Underage.AgeUnder13"), Lang.Text("Common.Option.IDontKnow")) == 1)
{
ModBase.OpenWebsite("https://account.live.com/editprof.aspx");
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.Microsoft.ChangeBirthDate.Message"),
Lang.Text("Minecraft.Launch.Login.Hint"));
}
else
{
ModBase.OpenWebsite(
"https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a");
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.Microsoft.ChangeBirthDate.SupportMessage"),
Lang.Text("Minecraft.Launch.Login.Hint"));
}
throw new Exception("$$");
}
ModProfile.ProfileLog("正版验证 Step 3/6 获取 XSTSToken 失败:" + response.StatusCode);
var isIgnore = false;
ModBase.RunInUiWait(() =>
{
if (!isLaunching)
return;
if (ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
isIgnore = true;
});
if (isIgnore)
{
return new[] { ModProfile.selectedProfile.AccessToken, "Ignore" };
return default;
}
response.EnsureSuccessStatusCode();
}
}
var resultJson = (JsonObject)ModBase.GetJson(result);
var xSTSToken = resultJson["Token"].ToString();
var uhs = resultJson["DisplayClaims"]["xui"][0]["uhs"].ToString();
return new[] { xSTSToken, uhs };
}
/// <summary>
/// 正版验证步骤 4:从 {XSTSToken, UHS} 获取 Minecraft accessToken
/// </summary>
/// <param name="tokens">包含 XSTSToken 与 UHS 的字符串组</param>
/// <returns>Minecraft accessToken</returns>
private static string MsLoginStep4(string[] tokens)
{
ModProfile.ProfileLog("开始正版验证 Step 4/6: 获取 Minecraft AccessToken");
if (tokens.Length < 2 || string.IsNullOrEmpty(tokens.ElementAt(0)) || string.IsNullOrEmpty(tokens.ElementAt(1)))
throw new ArgumentException("传入的 XSTSToken 或者 UHS 错误", nameof(tokens));
var requestData = new Dictionary<string, string> { { "identityToken", $"XBL3.0 x={tokens[1]};{tokens[0]}" } };
string result;
try
{
using (var response = HttpRequest
.CreatePost("https://api.minecraftservices.com/authentication/login_with_xbox")
.WithJsonContent(requestData)
.SendAsync()
.GetAwaiter()
.GetResult())
{
response.EnsureSuccessStatusCode();
result = response.AsString();
}
}
catch (HttpRequestException ex)
{
var message = ex.Message;
if (ex.StatusCode.Equals(HttpStatusCode.TooManyRequests))
{
ModBase.Log(ex, "正版验证 Step 4 汇报 429");
throw new Exception(Lang.Text("Minecraft.Launch.Login.Microsoft.TooManyRequests"));
}
if (ex.StatusCode is { } arg1 && arg1 == HttpStatusCode.Forbidden)
{
ModBase.Log(ex, "正版验证 Step 4 汇报 403");
throw new Exception(Lang.Text("Minecraft.Launch.Login.Microsoft.AbnormalIp"));
}
ModProfile.ProfileLog("正版验证 Step 4/6 获取 MC AccessToken 失败:" + ex);
var isIgnore = false;
ModBase.RunInUiWait(() =>
{
if (!isLaunching)
return;
if (ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
isIgnore = true;
});
if (isIgnore)
{
return "Ignore";
return default;
}
throw;
}
var resultJson = (JsonObject)ModBase.GetJson(result);
var accessToken = resultJson["access_token"].ToString();
if (string.IsNullOrWhiteSpace(accessToken))
throw new Exception("获取到的 Minecraft AccessToken 为空,登录流程异常!");
return accessToken;
}
/// <summary>
/// 正版验证步骤 5:验证微软账号是否持有 MC,这也会刷新 XGP
/// </summary>
/// <param name="accessToken">Minecraft accessToken</param>
private static void MsLoginStep5(string accessToken)
{
ModProfile.ProfileLog("开始正版验证 Step 5/6: 验证账户是否持有 MC");
if (string.IsNullOrEmpty(accessToken))
throw new ArgumentException("传入的 AccessToken 为空", nameof(accessToken));
var result = "";
try
{
using (var response = HttpRequest
.Create("https://api.minecraftservices.com/entitlements/mcstore")
.WithBearerToken(accessToken)
.SendAsync()
.GetAwaiter()
.GetResult())
{
response.EnsureSuccessStatusCode();
result = response.AsString();
}
var resultJson = (JsonObject)ModBase.GetJson(result);
if (!(resultJson.ContainsKey("items") && resultJson["items"].AsArray().Any(x =>
x["name"]?.ToString() == "product_minecraft" || x["name"]?.ToString() == "game_minecraft")))
{
switch (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.NotPurchased"),
Lang.Text("Minecraft.Launch.Login.Failed"), Lang.Text("Minecraft.Launch.Login.Microsoft.PurchaseMinecraft"), Lang.Text("Common.Action.Cancel")))
{
case 1:
{
ModBase.OpenWebsite(
"https://www.xbox.com/zh-cn/games/store/minecraft-java-bedrock-edition-for-pc/9nxp44l49shj");
break;
}
}
throw new Exception("$$");
}
}
catch (Exception ex)
{
ModBase.Log(ex, "正版验证 Step 5 异常:" + result);
throw;
}
}
/// <summary>
/// 正版验证步骤 6:从 Minecraft accessToken 获取 {UUID, UserName, ProfileJson}
/// </summary>
/// <param name="accessToken">Minecraft accessToken</param>
/// <returns>包含 UUID, UserName 和 ProfileJson 的字符串组</returns>
private static string[] MsLoginStep6(string accessToken)
{
ModProfile.ProfileLog("开始正版验证 Step 6/6: 获取玩家 ID 与 UUID 等相关信息");
if (string.IsNullOrEmpty(accessToken))
throw new ArgumentException("传入的 AccessToken 为空", nameof(accessToken));
string result;
try
{
using (var response = HttpRequest
.Create("https://api.minecraftservices.com/minecraft/profile")
.WithBearerToken(accessToken)
.SendAsync()
.GetAwaiter()
.GetResult())
{
response.EnsureSuccessStatusCode();
result = response.AsString();
}
}
catch (HttpRequestException ex)
{
var message = ex.Message;
if (ex.StatusCode.Equals(HttpStatusCode.TooManyRequests))
{
ModBase.Log(ex, "正版验证 Step 6 汇报 429");
throw new Exception(Lang.Text("Minecraft.Launch.Login.Microsoft.TooManyRequests"));
}
if (ex.StatusCode is { } arg2 && arg2 == HttpStatusCode.NotFound)
{
ModBase.Log(ex, "正版验证 Step 6 汇报 404");
ModBase.RunInNewThread(() =>
{
switch (ModMain.MyMsgBox(Lang.Text("Minecraft.Launch.Login.Microsoft.CreateProfile.Message"), Lang.Text("Minecraft.Launch.Login.Failed"), Lang.Text("Minecraft.Launch.Login.Microsoft.CreateProfile.Button"), Lang.Text("Common.Action.Cancel")))
{
case 1:
{
ModBase.OpenWebsite("https://www.minecraft.net/zh-hans/msaprofile/mygames/editprofile");
break;
}
}
}, "Login Failed: Create Profile");
throw new Exception("$$");
}
ModProfile.ProfileLog("正版验证 Step 6/6 获取玩家档案信息失败:" + ex);
var isIgnore = false;
ModBase.RunInUiWait(() =>
{
if (!isLaunching)
return;
if (ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Message"),
Lang.Text("Minecraft.Launch.Login.RefreshAccountFailed.Title"), Lang.Text("Minecraft.Launch.Login.Continue"), Lang.Text("Common.Action.Cancel")) == 1)
isIgnore = true;
});
if (isIgnore)
{
return new[] { ModProfile.selectedProfile.Uuid, ModProfile.selectedProfile.Username, "Ignore" };
return default;
}
throw;
}
var resultJson = (JsonObject)ModBase.GetJson(result);
var uuid = resultJson["id"].ToString();
var userName = resultJson["name"].ToString();
return new[] { uuid, userName, result };
}
#endregion
#region 第三方验证
private static void McLoginServerStart(ModLoader.LoaderTask<McLoginServer, McLoginResult> data)
{
var input = data.input;
var needRefresh = false;
var wasRefreshed = false;
ModProfile.ProfileLog("验证方式:" + input.Description);
data.Progress = 0.05d;
// 尝试验证登录(如果不需要重新选择档案且不是创建档案)
if (!input.ForceReselectProfile && !ModProfile.isCreatingProfile)
{
try
{
ThrowIfAborted(data);
McLoginRequestValidate(ref data);
data.Progress = 0.95d;
return; // 登录成功,直接返回
}
catch (WebException ex)
{
_HandleHttpWebException(ex, "验证登录失败");
}
catch (HttpResponseException ex){
ModProfile.ProfileLog($"验证登录失败: {ex}");
}
catch (Exception ex)
{
_HandleException(ex, "验证登录失败", "Minecraft.Launch.Login.Auth.ValidationFailed.WithDetail");
}
data.Progress = 0.25d;
// 尝试刷新登录
try
{
ThrowIfAborted(data);
McLoginRequestRefresh(ref data, needRefresh);
data.Progress = needRefresh ? 0.85d : 0.45d;
data.Progress = 0.95d;
return; // 刷新成功,直接返回
}
catch (Exception ex)
{
ModProfile.ProfileLog(Lang.Text("Minecraft.Launch.Login.Auth.RefreshFailed") + ": " + ex);
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.Auth.RefreshFailed.WithDetail", ex.ToString()),
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
isWarn: true);
if (wasRefreshed)
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.SecondRefreshFailed"), ex);
}
}
// 尝试普通登录
try
{
ThrowIfAborted(data);
needRefresh = McLoginRequestLogin(ref data);
}
catch (WebException ex)
{
_HandleLoginHttpException(ex);
}
catch (Exception ex)
{
_HandleException(ex, "第三方登录失败", "Minecraft.Launch.Login.Auth.LoginFailed.WithDetail");
}
// 如果需要刷新,循环刷新一次
if (needRefresh)
{
ModProfile.ProfileLog("重新进行刷新登录");
wasRefreshed = true;
data.Progress = 0.65d;
try
{
ThrowIfAborted(data);
McLoginRequestRefresh(ref data, needRefresh);
data.Progress = 0.95d;
return;
}
catch (Exception ex)
{
ModProfile.ProfileLog(Lang.Text("Minecraft.Launch.Login.Auth.RefreshFailed") + ": " + ex);
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Login.Auth.RefreshFailed.WithDetail", ex.ToString()),
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
isWarn: true);
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.SecondRefreshFailed"), ex);
}
}
// 最终完成
// 兜底校验:若走到这里仍未取得有效 AccessToken(例如回退登录的 HTTP 失败被 McLoginRequestLogin
// 吞掉并返回 false),说明登录实际失败,必须中止,避免带着空凭据继续启动(见 #3307 review)。
if (string.IsNullOrEmpty(data.output.AccessToken))
throw new Exception(Lang.Text("Minecraft.Launch.Login.Failed"));
data.Progress = 0.95d;
}
/// <summary>
/// 检查任务是否被中断
/// </summary>
private static void ThrowIfAborted(ModLoader.LoaderTask<McLoginServer, McLoginResult> data)
{
if (data.IsAborted)
throw new ThreadInterruptedException();
}
/// <summary>
/// 统一处理 HttpWebException
/// </summary>
private static void _HandleHttpWebException(WebException ex, string logPrefix)
{
var allMessage = ex.ToString();
ModProfile.ProfileLog(logPrefix + "" + allMessage);
if ((!allMessage.Contains("超时") && !allMessage.Contains("imeout"))
|| allMessage.Contains("403"))
return;
ModProfile.ProfileLog("已触发超时登录失败");
var message = Lang.Text("Minecraft.Launch.Login.Auth.Timeout.WithDetail", ex.ToString());
ModMain.MyMsgBox(
message,
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
isWarn: true);
throw new Exception("$" + message);
}
/// <summary>
/// 统一处理普通异常
/// </summary>
private static void _HandleException(
Exception ex,
string logPrefix,
string userMessageKey)
{
ModProfile.ProfileLog(logPrefix + "" + ex);
var message = Lang.Text(userMessageKey, ex.ToString());
ModMain.MyMsgBox(
message,
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
isWarn: true);
throw new Exception("$" + message);
}
/// <summary>
/// 处理普通登录 HttpWebException
/// </summary>
private static void _HandleLoginHttpException(WebException ex)
{
ModProfile.ProfileLog("验证失败:" + ex);
var message = Lang.Text("Minecraft.Launch.Login.Auth.NetworkFailed.WithDetail", ex.ToString());
ModMain.MyMsgBox(
message,
Lang.Text("Minecraft.Launch.Login.Auth.FailedTitle"),
isWarn: true);
throw new Exception("$" + message);
}
// Server 登录:三种验证方式的请求
private static void McLoginRequestValidate(ref ModLoader.LoaderTask<McLoginServer, McLoginResult> data)
{
ModProfile.ProfileLog("验证登录开始(Validate, Authlib");
// 提前缓存信息,否则如果在登录请求过程中退出登录,设置项目会被清空,导致输出存在空值
var accessToken = "";
var clientToken = "";
var uuid = "";
var name = "";
if (ModProfile.selectedProfile is not null)
{
accessToken = ModProfile.selectedProfile.AccessToken;
clientToken = ModProfile.selectedProfile.ClientToken;
uuid = ModProfile.selectedProfile.Uuid;
name = ModProfile.selectedProfile.Username;
}
// 发送登录请求
var requestData = new JsonObject { ["accessToken"] = accessToken, ["clientToken"] = clientToken };
Requester.Fetch(data.input.BaseUrl + "/validate",
new FetchParam
{
Method = "POST",
Content = requestData.ToJsonString(),
Headers = new Dictionary<string, string> { { "Accept-Language", "zh-CN" } },
ContentType = "application/json"
}); // 没有返回值的
// 将登录结果输出
data.output.AccessToken = accessToken;
data.output.ClientToken = clientToken;
data.output.Uuid = uuid;
data.output.Name = name;
data.output.Type = "Auth";
// 不更改缓存,直接结束
ModProfile.ProfileLog("验证登录成功(Validate, Authlib");
}
private static void McLoginRequestRefresh(ref ModLoader.LoaderTask<McLoginServer, McLoginResult> data,
bool requestUser)
{
try
{
var refreshInfo = new JsonObject();
var selectProfile = new JsonObject
{ { "name", ModProfile.selectedProfile.Username }, { "id", ModProfile.selectedProfile.Uuid } };
refreshInfo.Add("selectedProfile", selectProfile);
refreshInfo.Add("accessToken", ModProfile.selectedProfile.AccessToken);
refreshInfo.Add("requestUser", true);
ModProfile.ProfileLog("刷新登录开始(Refresh, Authlib");
var loginJson = (JsonObject)ModBase.GetJson(Requester.Fetch(data.input.BaseUrl + "/refresh",
new FetchParam
{
Method = "POST",
Content = refreshInfo.ToJsonString(),
Headers = new Dictionary<string, string> { { "Accept-Language", "zh-CN" } },
ContentType = "application/json",
RequireContent = true
}
));
// 将登录结果输出
if (loginJson["selectedProfile"] is null)
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.InvalidProfile", ModProfile.selectedProfile.Username));
data.output.AccessToken = loginJson["accessToken"].ToString();
data.output.ClientToken = loginJson["clientToken"].ToString();
data.output.Uuid = loginJson["selectedProfile"]["id"].ToString();
data.output.Name = loginJson["selectedProfile"]["name"].ToString();
data.output.Type = "Auth";
// 保存缓存
var profileIndex = ModProfile.profileList.IndexOf(ModProfile.selectedProfile);
ModProfile.profileList[profileIndex].Username = data.output.Name;
ModProfile.profileList[profileIndex].AccessToken = data.output.AccessToken;
ModProfile.profileList[profileIndex].ClientToken = data.output.ClientToken;
ModProfile.profileList[profileIndex].Uuid = data.output.Uuid;
ModProfile.profileList[profileIndex].Name = data.input.UserName;
ModProfile.profileList[profileIndex].Password = data.input.Password;
ModProfile.ProfileLog("刷新登录成功(Refresh, Authlib");
}
catch (HttpResponseException ex)
{
// 刷新失败必须向上抛出:否则 McLoginServerStart 会把本次登录判为“刷新成功”、带着空令牌继续
// 启动,并丧失“回退到普通登录”的自动恢复机会。保留服务端错误详情作为消息,并把原始
// HttpResponseException(含状态码/堆栈)作为 InnerException 以便诊断;同时显式 Dispose 及时
// 释放底层 Response,不依赖终结器兜底(其回收时机不确定,可能令底层资源驻留)。
var message = _TryGetLastError(ex, out var detail) ? detail : ex.Message;
ex.Dispose();
throw new Exception(message, ex);
}
}
private static bool McLoginRequestLogin(ref ModLoader.LoaderTask<McLoginServer, McLoginResult> data)
{
try
{
var needRefresh = false;
ModProfile.ProfileLog("登录开始(Login, Authlib");
var requestData = new JsonObject
{
["agent"] = new JsonObject { ["name"] = "Minecraft", ["version"] = 1 },
["username"] = data.input.UserName,
["password"] = data.input.Password,
["requestUser"] = true
};
var loginJson = (JsonObject)ModBase.GetJson(Requester.Fetch(data.input.BaseUrl + "/authenticate",
new FetchParam
{
Method = "POST",
Content = requestData.ToJsonString(),
Headers = new Dictionary<string, string> { { "Accept-Language", "zh-CN" } },
ContentType = "application/json",
RequireContent = true
}));
// 检查登录结果
if (loginJson["availableProfiles"].AsArray().Count == 0)
{
if (data.input.ForceReselectProfile)
HintService.Hint(Lang.Text("Minecraft.Launch.Login.Auth.NoProfileCannotSwitch"), HintType.Error);
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.NoProfile"));
}
if (data.input.ForceReselectProfile && loginJson["availableProfiles"].AsArray().Count == 1)
HintService.Hint(Lang.Text("Minecraft.Launch.Login.Auth.OnlyOneProfile"), HintType.Error);
string selectedName = null;
string selectedId = null;
if ((loginJson["selectedProfile"] is null || data.input.ForceReselectProfile) &&
loginJson["availableProfiles"].AsArray().Count > 1)
{
// 要求选择档案;优先从缓存读取
needRefresh = true;
var cacheId = ModProfile.selectedProfile is not null ? ModProfile.selectedProfile.Uuid : "";
foreach (var profile in loginJson["availableProfiles"].AsArray())
if ((profile["id"].ToString() ?? "") == (cacheId ?? ""))
{
selectedName = profile["name"].ToString();
selectedId = profile["id"].ToString();
ModProfile.ProfileLog("根据缓存选择的角色:" + selectedName);
}
// 缓存无效,要求玩家选择
if (selectedName is null)
{
ModProfile.ProfileLog("要求玩家选择角色");
ModBase.RunInUiWait(() =>
{
var selectionControl = new List<IMyRadio>();
var selectionJson = new List<JsonNode>();
foreach (var profile in loginJson["availableProfiles"].AsArray())
{
selectionControl.Add(new MyRadioBox { Text = profile["name"].ToString() });
selectionJson.Add(profile);
}
var selectedIndex = (int)ModMain.MyMsgBoxSelect(selectionControl, Lang.Text("Minecraft.Launch.Login.Auth.SelectProfile"));
selectedName = selectionJson[selectedIndex]["name"].ToString();
selectedId = selectionJson[selectedIndex]["id"].ToString();
});
ModProfile.ProfileLog("玩家选择的角色:" + selectedName);
}
}
else
{
selectedName = loginJson["selectedProfile"]["name"].ToString();
selectedId = loginJson["selectedProfile"]["id"].ToString();
}
// 将登录结果输出
data.output.AccessToken = loginJson["accessToken"].ToString();
data.output.ClientToken = loginJson["clientToken"].ToString();
data.output.Name = selectedName;
data.output.Uuid = selectedId;
data.output.Type = "Auth";
// 获取服务器信息
var response =
Requester.FetchString(data.input.BaseUrl.Replace("/authserver", ""));
var serverName = ModBase.GetJson(response)["meta"]?["serverName"]?.ToString() ?? data.input.BaseUrl.Replace("/authserver", "");
// 保存缓存
if (data.input.IsExist)
{
var profileIndex = ModProfile.profileList.IndexOf(ModProfile.selectedProfile);
ModProfile.profileList[profileIndex].Username = data.output.Name;
ModProfile.profileList[profileIndex].Uuid = data.output.Uuid;
ModProfile.profileList[profileIndex].ServerName = serverName;
ModProfile.profileList[profileIndex].AccessToken = data.output.AccessToken;
ModProfile.profileList[profileIndex].ClientToken = data.output.ClientToken;
}
else
{
var newProfile = new ModProfile.McProfile
{
Type = McLoginType.Auth,
Uuid = data.output.Uuid,
Username = data.output.Name,
Server = data.input.BaseUrl,
ServerName = serverName,
Name = data.input.UserName,
Password = data.input.Password,
AccessToken = data.output.AccessToken,
ClientToken = data.output.ClientToken,
Expires = 1743779140286L,
Desc = ""
};
ModProfile.profileList.Add(newProfile);
ModProfile.selectedProfile = newProfile;
ModProfile.isCreatingProfile = false;
}
ModProfile.SaveProfile();
ModProfile.ProfileLog("登录成功(Login, Authlib");
return needRefresh;
}
catch (HttpResponseException ex)
{
if (_TryGetLastError(ex, out var message)) ModMain.MyMsgBox(message, Lang.Text("Minecraft.Launch.Login.Failed"));
ex.Dispose();
return false;
}
catch (Exception ex)
{
ModProfile.ProfileLog($"第三方验证失败: {ex}");
if (ex.Message.StartsWithF("$")) throw;
throw new Exception(Lang.Text("Minecraft.Launch.Login.Auth.LoginFailed", ex.Message), ex);
}
}
private static bool _TryGetLastError(HttpResponseException ex,[NotNullWhen(true)] out string? message)
{
message = null;
try
{
using var responseStream = ex.Response?.Content.ReadAsStream();
if (responseStream is null) return false;
var result = JsonSerializer.Deserialize<YggdrasilAuthenticateResult>(responseStream, JsonCompat.SerializerOptions);
if (result?.ErrorMessage is null) return false;
message = result.ErrorMessage;
return true;
}
catch (Exception)
{
// Suppress Exception
}
return false;
}
#endregion
#region 离线验证
private static void McLoginLegacyStart(ModLoader.LoaderTask<McLoginLegacy, McLoginResult> data)
{
var input = data.input;
ModProfile.ProfileLog($"验证方式:离线({input.UserName}, {input.Uuid}");
data.Progress = 0.1d;
{
ref var withBlock = ref data.output;
withBlock.Name = input.UserName;
withBlock.Uuid = ModProfile.selectedProfile.Uuid;
withBlock.Type = "Legacy";
}
// 将结果扩展到所有项目中
data.output.AccessToken = data.output.Uuid;
data.output.ClientToken = data.output.Uuid;
}
#endregion
#endregion
#region Java 处理
public static JavaEntry mcLaunchJavaSelected;
private static void McLaunchJava(ModLoader.LoaderTask<int, int> task)
{
var minVer = new Version(0, 0, 0, 0);
var maxVer = new Version(999, 999, 999, 999);
// MC 大版本检测
if ((!ModInstanceList.McMcInstanceSelected.Info.Valid &&
ModInstanceList.McMcInstanceSelected.releaseTime >= new DateTime(2024, 4, 2)) ||
(ModInstanceList.McMcInstanceSelected.Info.Valid &&
ModInstanceList.McMcInstanceSelected.Info.vanilla >= new Version(20, 0, 5)))
{
// 1.20.5+ (24w14a+):至少 Java 21
if (ModBase.modeDebug)
ModBase.Log("[Launch] [Debug] MC 1.20.5+ (24w14a+) 要求至少 Java 21");
minVer = new Version(21, 0, 0, 0);
}
else if ((!ModInstanceList.McMcInstanceSelected.Info.Valid &&
ModInstanceList.McMcInstanceSelected.releaseTime >= new DateTime(2021, 11, 16)) ||
(ModInstanceList.McMcInstanceSelected.Info.Valid &&
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 18))
{
// 1.18 pre2+:至少 Java 17
if (ModBase.modeDebug)
ModBase.Log("[Launch] [Debug] MC 1.18 pre2+ 要求至少 Java 17");
minVer = new Version(17, 0, 0, 0);
}
else if ((!ModInstanceList.McMcInstanceSelected.Info.Valid &&
ModInstanceList.McMcInstanceSelected.releaseTime >= new DateTime(2021, 5, 11)) ||
(ModInstanceList.McMcInstanceSelected.Info.Valid &&
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 17))
{
// 1.17+ (21w19a+):至少 Java 16
if (ModBase.modeDebug)
ModBase.Log("[Launch] [Debug] MC 1.17+ (21w19a+) 要求至少 Java 16");
minVer = new Version(16, 0, 0, 0);
}
else if (ModInstanceList.McMcInstanceSelected.releaseTime.Year >= 2017) // Minecraft 1.12 与 1.11 的分界线正好是 2017 年,太棒了
{
// 1.12+:至少 Java 8
if (ModBase.modeDebug)
ModBase.Log("[Launch] [Debug] MC 1.12+ 要求至少 Java 8");
minVer = new Version(1, 8, 0, 0);
}
else if (ModInstanceList.McMcInstanceSelected.releaseTime <= new DateTime(2013, 5, 1) &&
ModInstanceList.McMcInstanceSelected.releaseTime.Year >= 2001) // 避免某些版本写个 1960 年
{
// 1.5.2-:最高 Java 8
if (ModBase.modeDebug)
ModBase.Log("[Launch] [Debug] MC 1.5.2- 要求最高 Java 12");
maxVer = new Version(1, 8, 999, 999);
}
// 原版 26+:获取 Mojang 要求的 Java 版本
string recommendedComponent = null;
var recommendedCode =
ModInstanceList.McMcInstanceSelected.JsonObject?["javaVersion"]?["majorVersion"]?.ToObject<int>() ??
ModInstanceList.McMcInstanceSelected.JsonVersion?["java_version"]?.ToObject<int>() ?? 0;
if (recommendedCode >= 22)
{
McLaunchLog("Mojang 要求至少使用 Java " + recommendedCode);
minVer = new Version(recommendedCode, 0, 0, 0);
recommendedComponent =
ModInstanceList.McMcInstanceSelected.JsonObject?["javaVersion"]?["component"]?.ToString() ??
ModInstanceList.McMcInstanceSelected.JsonVersion?["java_component"]?.ToString();
if (string.IsNullOrEmpty(recommendedComponent))
recommendedComponent = null;
}
// OptiFine 检测
if (ModInstanceList.McMcInstanceSelected.Info.HasOptiFine && ModInstanceList.McMcInstanceSelected.Info.Valid) // 不管非标准版本
{
if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major < 7)
{
// <1.7:至多 Java 8
maxVer = new Version(1, 8, 999, 999);
}
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 8 &&
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major < 12)
{
// 1.8 - 1.11:必须恰好 Java 8
minVer = new Version(1, 8, 0, 0);
maxVer = new Version(1, 8, 999, 999);
}
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major == 12)
{
// 1.12:最高 Java 8
maxVer = new Version(1, 8, 999, 999);
}
}
// Forge 检测
if (ModInstanceList.McMcInstanceSelected.Info.HasForge)
{
if (ModInstanceList.McMcInstanceSelected.Info.vanilla >= new Version(6, 0, 1) &&
ModInstanceList.McMcInstanceSelected.Info.vanilla <= new Version(7, 0, 2))
{
// 1.6.1 - 1.7.2:必须 Java 7
minVer = new Version(1, 7, 0, 0) > minVer ? new Version(1, 7, 0, 0) : minVer;
maxVer = new Version(1, 7, 999, 999) < maxVer ? new Version(1, 7, 999, 999) : maxVer;
}
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major <= 12 ||
!ModInstanceList.McMcInstanceSelected.Info.Valid) // 非标准版本
{
// <=1.12Java 8
maxVer = new Version(1, 8, 999, 999);
}
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major <= 14)
{
// 1.13 - 1.14Java 8 - 10
minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer;
maxVer = new Version(1, 10, 999, 999) < maxVer ? new Version(1, 10, 999, 999) : maxVer;
}
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major == 15)
{
// 1.15Java 8 - 15
minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer;
maxVer = new Version(1, 15, 999, 999) < maxVer ? new Version(1, 15, 999, 999) : maxVer;
}
else if (McVersionComparer.CompareVersionGe(ModInstanceList.McMcInstanceSelected.Info.Forge, "34.0.0") &&
McVersionComparer.CompareVersionGe("36.2.25", ModInstanceList.McMcInstanceSelected.Info.Forge))
{
// 1.16Forge 34.X ~ 36.2.25:最高 Java 8u321
maxVer = new Version(1, 8, 0, 320) < maxVer ? new Version(1, 8, 0, 321) : maxVer;
}
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 18 &&
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major < 19 &&
ModInstanceList.McMcInstanceSelected.Info.HasOptiFine) // #305
{
// 1.18:若安装了 OptiFine,最高 Java 18
maxVer = new Version(1, 18, 999, 999) < maxVer ? new Version(1, 18, 999, 999) : maxVer;
}
}
// Cleanroom 检测
if (ModInstanceList.McMcInstanceSelected.Info.HasCleanroom)
{
if (!Version.TryParse(ModInstanceList.McMcInstanceSelected.Info.Cleanroom.Split('-')[0], out Version cleanroomVersion))
throw new FormatException("无法解析 Cleanroom 版本号:" + ModInstanceList.McMcInstanceSelected.Info.Cleanroom);
if (cleanroomVersion < new Version(0, 5, 0, 0))
{
if (ModBase.modeDebug) ModBase.Log("[Launch] [Debug] Cleanroom 版本低于 0.5,要求至少 Java 21");
minVer = new Version(21, 0, 0, 0) > minVer ? new Version(21, 0, 0, 0) : minVer;
}
else
{
if (ModBase.modeDebug) ModBase.Log("[Launch] [Debug] Cleanroom 版本高于 0.5,要求至少 Java 25");
minVer = new Version(25, 0, 0, 0) > minVer ? new Version(25, 0, 0, 0) : minVer;
}
}
// Fabric 检测
if (ModInstanceList.McMcInstanceSelected.Info.HasFabric && ModInstanceList.McMcInstanceSelected.Info.Valid) // 不管非标准版本
{
if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 15 &&
ModInstanceList.McMcInstanceSelected.Info.vanilla.Major <= 16)
// 1.15 - 1.16Java 8+
minVer = new Version(1, 8, 0, 0) > minVer ? new Version(1, 8, 0, 0) : minVer;
else if (ModInstanceList.McMcInstanceSelected.Info.vanilla.Major >= 18)
// 1.18+Java 17+
minVer = new Version(1, 17, 0, 0) > minVer ? new Version(1, 17, 0, 0) : minVer;
}
// LiteLoader 检测
if (ModInstanceList.McMcInstanceSelected.Info.HasLiteLoader && ModInstanceList.McMcInstanceSelected.Info.Valid)
{
// 最高 Java 8
if (ModBase.modeDebug)
ModBase.Log("[Launch] [Debug] LiteLoader 要求最高 Java 8");
maxVer = new Version(8, 999, 999, 999) < maxVer ? new Version(8, 999, 999, 999) : maxVer;
}
// LabyMod 检测
if (ModInstanceList.McMcInstanceSelected.Info.HasLabyMod)
{
if (ModBase.modeDebug)
ModBase.Log("[Launch] [Debug] LabyMod 要求至少 Java 21");
minVer = new Version(21, 0, 0, 0) > minVer ? new Version(21, 0, 0, 0) : minVer;
maxVer = new Version(999, 999, 999, 999);
}
// JSON 中要求的版本
if (ModInstanceList.McMcInstanceSelected.JsonObject["javaVersion"] is not null)
{
var majorVersion = ModBase.Val(ModInstanceList.McMcInstanceSelected.JsonObject["javaVersion"]["majorVersion"]);
if (ModBase.modeDebug)
ModBase.Log("[Launch] [Debug] JSON 中参数要求至少 Java " + majorVersion);
if (majorVersion <= 8d)
minVer = new Version(1, (int)Math.Round(majorVersion), 0, 0) > minVer
? new Version(1, (int)Math.Round(majorVersion), 0, 0)
: minVer;
else
minVer = new Version((int)Math.Round(majorVersion), 0, 0, 0) > minVer
? new Version((int)Math.Round(majorVersion), 0, 0, 0)
: minVer;
if (maxVer < minVer)
maxVer = new Version(999, 999, 999, 999);
}
lock (ModJava.javaLock)
{
// 选择 Java
McLaunchLog("Java 版本需求:最低 " + minVer + ",最高 " + maxVer);
mcLaunchJavaSelected = ModJava.JavaSelect("$$", minVer, maxVer, ModInstanceList.McMcInstanceSelected);
if (task.IsAborted)
return;
if (mcLaunchJavaSelected is not null)
{
McLaunchLog("选择的 Java" + mcLaunchJavaSelected);
return;
}
// 无合适的 Java
if (task.IsAborted)
return; // 中断加载会导致 JavaSelect 异常地返回空值,误判找不到 Java
McLaunchLog("无合适的 Java,需要确认是否自动下载");
string javaCode;
if (minVer >= new Version(1, 9))
{
javaCode = minVer.Major.ToString();
}
else if (maxVer < new Version(1, 8))
{
if (ModInstanceList.McMcInstanceSelected.Info.HasForge)
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Java.NeedLegacyJavaFixerOrJava7"),
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
else
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Java.NeedJava7"),
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
throw new Exception("$$");
}
else if (minVer > new Version(1, 8, 0, 140) && maxVer < new Version(1, 8, 0, 321))
{
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Java.NeedJava8U141ToU320"),
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
throw new Exception("$$");
}
else if (minVer > new Version(1, 8, 0, 140))
{
ModMain.MyMsgBox(
Lang.Text("Minecraft.Launch.Java.NeedJava8U141OrLater"),
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
throw new Exception("$$");
}
else
{
javaCode = 8.ToString();
}
if (!ModJava.JavaDownloadConfirm($"Java {javaCode}"))
throw new Exception("$$");
// 开始自动下载
var javaLoader = ModJava.GetJavaDownloadLoader();
try
{
javaLoader.Start(recommendedComponent ?? javaCode, true); // 在 Java 22+ 时优先使用 Mojang 提供的 Component 字段
while (javaLoader.State == ModBase.LoadState.Loading && !task.IsAborted)
{
task.Progress = javaLoader.Progress;
Thread.Sleep(10);
}
}
finally
{
javaLoader.Abort(); // 确保取消时中止 Java 下载
}
// 检查下载结果
mcLaunchJavaSelected = ModJava.JavaSelect("$$", minVer, maxVer, ModInstanceList.McMcInstanceSelected);
if (task.IsAborted)
return;
if (mcLaunchJavaSelected is not null)
{
McLaunchLog("选择的 Java" + mcLaunchJavaSelected);
}
else
{
HintService.Hint(Lang.Text("Minecraft.Launch.Error.NoJava"), HintType.Error);
throw new Exception("$$");
}
}
}
#endregion
#region 启动参数
internal static void SecretLaunchJvmArgs(ref List<string> dataList)
{
var dataJvmCustom = Config.Instance.JvmArgs[ModInstanceList.McMcInstanceSelected?.PathInstance];
dataList.Insert(0,
string.IsNullOrEmpty(dataJvmCustom)
? Config.Launch.JvmArgs
: dataJvmCustom); // 可变 JVM 参数
switch (Config.Launch.PreferredIpStack)
{
case JvmPreferredIpStack.PreferV4:
{
dataList.Add("-Djava.net.preferIPv4Stack=true");
dataList.Add("-Djava.net.preferIPv4Addresses=true");
break;
}
case JvmPreferredIpStack.PreferV6:
{
dataList.Add("-Djava.net.preferIPv6Stack=true");
dataList.Add("-Djava.net.preferIPv6Addresses=true");
break;
}
}
double availableGb = KernelInterop.GetAvailablePhysicalMemoryBytes() / 1073741824.0;
ModLaunch.McLaunchLog($"当前剩余内存:{availableGb.ToString("N1", CultureInfo.InvariantCulture)}G");
double totalRamMb = PageInstanceSetup.GetRam(ModInstanceList.McMcInstanceSelected) * 1024d;
var maxHeapArg = Math.Floor(totalRamMb).ToString(CultureInfo.InvariantCulture);
dataList.Add("-Xmn" + Math.Floor(totalRamMb * 0.15).ToString(CultureInfo.InvariantCulture) + "m");
dataList.Add("-Xmx" + maxHeapArg + "m");
// #3282: 固定堆大小时追加 -Xms 使其等于 -Xmx(复用同一数值以保持一致),隐式禁用内存归还降低延迟抖动、利于 ZGC。
// 若 dataList 中已存在 -Xms(例如用户自定义参数已设)则跳过,避免重复/冲突。
if (Config.Launch.LockMemory && !dataList.Any(d => d.Contains("-Xms", StringComparison.OrdinalIgnoreCase)))
dataList.Add("-Xms" + maxHeapArg + "m");
if (!dataList.Any(d => d.Contains("-Dlog4j2.formatMsgNoLookups=true")))
dataList.Add("-Dlog4j2.formatMsgNoLookups=true");
}
public class LaunchArgument
{
private readonly List<string> _features = new();
public LaunchArgument(McInstance minecraft)
{
var curArgu = string.Empty;
if (minecraft.IsOldJson)
_features = minecraft.JsonObject["minecraftArguments"].ToString().Split(' ').ToList();
else
foreach (var item in minecraft.JsonObject["arguments"]["game"].AsArray())
if (item.GetValueKind() == JsonValueKind.String)
_features.Add(item.ToString());
else if (item.GetValueKind() == JsonValueKind.Object)
{
var valueNode = item["value"];
if (valueNode.GetValueKind() == JsonValueKind.Array)
_features.AddRange(valueNode.AsArray().Select(x => x.ToString()));
else if (valueNode.GetValueKind() == JsonValueKind.String)
_features.Add(valueNode.ToString());
}
}
public object HasArguments(string key)
{
return _features.Contains(key);
}
}
private static string mcLaunchArgument;
/// <summary>
/// 释放 Java Wrapper 并返回完整文件路径。
/// </summary>
public static string ExtractJavaWrapper()
{
var wrapperPath = Path.Combine(ModBase.pathPure, "JavaWrapper.jar");
ModBase.Log("[Java] 选定的 Java Wrapper 路径:" + wrapperPath);
lock (extractJavaWrapperLock) // 避免 OptiFine 和 Forge 安装时同时释放 Java Wrapper 导致冲突
{
try
{
WriteJavaWrapper(wrapperPath);
}
catch (Exception ex)
{
if (File.Exists(wrapperPath))
{
// 因为未知原因 Java Wrapper 可能变为只读文件(#4243)
ModBase.Log(ex, "Java Wrapper 文件释放失败,但文件已存在,将在删除后尝试重新生成", ModBase.LogLevel.Developer);
try
{
File.Delete(wrapperPath);
WriteJavaWrapper(wrapperPath);
}
catch (Exception ex2)
{
ModBase.Log(ex2, "Java Wrapper 文件重新释放失败,将尝试更换文件名重新生成", ModBase.LogLevel.Developer);
wrapperPath = Path.Combine(ModBase.pathPure, "JavaWrapper2.jar");
try
{
WriteJavaWrapper(wrapperPath);
}
catch (Exception ex3)
{
throw new FileNotFoundException("释放 Java Wrapper 最终尝试失败", ex3);
}
}
}
else
{
throw new FileNotFoundException("释放 Java Wrapper 失败", ex);
}
}
}
return wrapperPath;
}
private static readonly object extractJavaWrapperLock = new();
private static void WriteJavaWrapper(string path)
{
ModBase.WriteFile(path, ModBase.GetResourceStream("Resources/java-wrapper.jar"));
}
/// <summary>
/// 释放 linkd 并返回完整文件路径。
/// </summary>
public static string ExtractLinkD()
{
var linkDPath = Path.Combine(ModBase.pathPure, "linkd.exe");
lock (extractLinkDLock) // 避免 OptiFine 和 Forge 安装时同时释放 Java Wrapper 导致冲突
{
try
{
WriteLinkD(linkDPath);
}
catch (Exception ex)
{
if (File.Exists(linkDPath))
{
ModBase.Log(ex, "linkd 文件释放失败,但文件已存在,将在删除后尝试重新生成", ModBase.LogLevel.Developer);
try
{
File.Delete(linkDPath);
WriteLinkD(linkDPath);
}
catch (Exception ex2)
{
throw new FileNotFoundException("释放 linkd 失败", ex2);
}
}
else
{
throw new FileNotFoundException("释放 linkd 失败", ex);
}
}
}
return linkDPath;
}
private static readonly object extractLinkDLock = new();
private static void WriteLinkD(string path)
{
ModBase.WriteFile(path, ModBase.GetResourceStream("Resources/linkd.exe"));
}
/// <summary>
/// 判断是否使用 LegacyFix。
/// </summary>
private static bool McLaunchNeedsLegacyFix(McInstance mc)
{
if (Config.Launch.DisableLF || Config.Instance.DisableLF[mc.PathInstance])
{
ModBase.Log("[Launch] LegacyFix 已被禁用");
return false;
}
if (mc.releaseTime < new DateTime(2013, 6, 25) && mc.releaseTime.Year > 2000)
{
return true;
}
return false;
}
/// <summary>
/// 获取实例所依赖的 LWJGL 版本
/// </summary>
private static string McLaunchGetLwjglVersion(McInstance mc)
{
foreach (ModLibrary.McLibToken library in ModLibrary.McLibListGet(mc, false))
{
if (string.IsNullOrWhiteSpace(library.OriginalName))
continue;
string[] parts = library.OriginalName.Split(':');
if (parts.Length >= 3 &&
parts[0].Equals("org.lwjgl", StringComparison.OrdinalIgnoreCase) &&
parts[1].Equals("lwjgl", StringComparison.OrdinalIgnoreCase))
{
return parts[2];
}
}
return null;
}
/// <summary>
/// 判断是否启用了针对 Minecraft 26.1 的性能问题补丁
/// </summary>
private static bool McLaunchUsesLwjglUnsafeAgent(McInstance mc)
{
if (McLaunchGetLwjglVersion(mc) == "3.4.1")
{
bool globalDisabled = Config.Launch.DisableLwjglUnsafeAgent;
bool instanceDisabled = Config.Instance.DisableLwjglUnsafeAgent[mc.PathInstance];
return !globalDisabled && !instanceDisabled;
}
else
{
return false;
}
}
// 主方法,合并 Jvm、Game、Replace 三部分的参数数据
private static void McLaunchArgumentMain(ModLoader.LoaderTask<string, List<ModLibrary.McLibToken>> loader)
{
McLaunchLog("开始获取 Minecraft 启动参数");
// 获取基准字符串与参数信息
string arguments;
if (ModInstanceList.McMcInstanceSelected.JsonObject["arguments"] is not null &&
ModInstanceList.McMcInstanceSelected.JsonObject["arguments"]["jvm"] is not null)
{
McLaunchLog("获取新版 JVM 参数");
arguments = McLaunchArgumentsJvmNew(ModInstanceList.McMcInstanceSelected);
McLaunchLog("新版 JVM 参数获取成功:");
McLaunchLog(arguments);
}
else
{
McLaunchLog("获取旧版 JVM 参数");
arguments = McLaunchArgumentsJvmOld(ModInstanceList.McMcInstanceSelected);
McLaunchLog("旧版 JVM 参数获取成功:");
McLaunchLog(arguments);
}
if (!string.IsNullOrEmpty(
(string)ModInstanceList.McMcInstanceSelected.JsonObject["minecraftArguments"])) // 有的实例 JSON 中是空字符串
{
McLaunchLog("获取旧版 Game 参数");
arguments += " " + McLaunchArgumentsGameOld(ModInstanceList.McMcInstanceSelected);
McLaunchLog("旧版 Game 参数获取成功");
}
if (ModInstanceList.McMcInstanceSelected.JsonObject["arguments"] is not null &&
ModInstanceList.McMcInstanceSelected.JsonObject["arguments"]["game"] is not null)
{
McLaunchLog("获取新版 Game 参数");
arguments += " " + McLaunchArgumentsGameNew(ModInstanceList.McMcInstanceSelected);
McLaunchLog("新版 Game 参数获取成功");
}
// 编码参数(#4700、#5892、#5909
if (mcLaunchJavaSelected.Installation.MajorVersion > 8)
{
if (!arguments.Contains("-Dstdout.encoding="))
arguments = "-Dstdout.encoding=UTF-8 " + arguments;
if (!arguments.Contains("-Dstderr.encoding="))
arguments = "-Dstderr.encoding=UTF-8 " + arguments;
}
if (mcLaunchJavaSelected.Installation.MajorVersion >= 18)
if (!arguments.Contains("-Dfile.encoding="))
arguments = "-Dfile.encoding=COMPAT " + arguments;
// MJSB
arguments = arguments.Replace(" -Dos.name=Windows 10", " -Dos.name=\"Windows 10\"");
// 全屏
if (Config.Launch.GameWindowMode == 0)
arguments += " --fullscreen";
// 由 Option 传入的额外参数
foreach (var arg in currentLaunchOptions.ExtraArgs)
arguments += " " + arg.Trim();
// 自定义参数
var argumentGame = Config.Instance.GameArgs[ModInstanceList.McMcInstanceSelected?.PathInstance];
arguments = arguments + " " + (string.IsNullOrEmpty(argumentGame) ? Config.Launch.GameArgs : argumentGame);
// 替换参数
var replaceArguments = McLaunchArgumentsReplace(ModInstanceList.McMcInstanceSelected, ref loader);
if (string.IsNullOrWhiteSpace(replaceArguments["${version_type}"]))
{
// 若自定义信息为空,则去掉该部分
arguments = arguments.Replace(" --versionType ${version_type}", "");
replaceArguments["${version_type}"] = "\"\"";
}
var finalArguments = "";
foreach (var argumentRaw in arguments.Split(" "))
{
var argument = argumentRaw;
foreach (var entry in replaceArguments)
argument = argument.Replace(entry.Key, entry.Value);
if ((argument.Contains(" ") || argument.Contains(@":\")) && !argument.EndsWithF("\""))
argument = $"\"{argument}\"";
finalArguments += argument + " ";
}
finalArguments = finalArguments.TrimEnd();
// 进存档
var worldName = currentLaunchOptions.WorldName;
if (worldName is not null) finalArguments += $" --quickPlaySingleplayer \"{worldName}\"";
// 进服
var server = string.IsNullOrEmpty(currentLaunchOptions.ServerIp)
? Config.Instance.ServerToEnter[ModInstanceList.McMcInstanceSelected?.PathInstance]
: currentLaunchOptions.ServerIp;
if (string.IsNullOrWhiteSpace(worldName) && !string.IsNullOrWhiteSpace(server))
{
if (ModInstanceList.McMcInstanceSelected.releaseTime > new DateTime(2023, 4, 4))
{
// QuickPlay
finalArguments += $" --quickPlayMultiplayer \"{server}\"";
}
else
{
// 老版本
if (server.Contains(":"))
// 包含端口号
finalArguments += " --server " + server.Split(":")[0] + " --port " + server.Split(":")[1];
else
// 不包含端口号
finalArguments += " --server " + server + " --port 25565";
if (ModInstanceList.McMcInstanceSelected.Info.HasOptiFine)
HintService.Hint(Lang.Text("Minecraft.Launch.Error.OptiFineAutoJoinWarning"), HintType.Error);
}
}
// 输出
McLaunchLog("Minecraft 启动参数:");
McLaunchLog(finalArguments);
mcLaunchArgument = finalArguments;
}
// Jvm 部分(第一段)
private static string McLaunchArgumentsJvmOld(McInstance instance)
{
// 存储以空格为间隔的启动参数列表
var dataList = new List<string>();
// 输出固定参数
dataList.Add("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump");
var argumentJvm = Config.Instance.JvmArgs[ModInstanceList.McMcInstanceSelected?.PathInstance];
if (string.IsNullOrEmpty(argumentJvm))
argumentJvm = Config.Launch.JvmArgs;
if (!argumentJvm.Contains("-Dlog4j2.formatMsgNoLookups=true"))
argumentJvm += " -Dlog4j2.formatMsgNoLookups=true";
argumentJvm = argumentJvm.Replace(" -XX:MaxDirectMemorySize=256M", ""); // #3511 的清理
dataList.Insert(0, argumentJvm); // 可变 JVM 参数
dataList.Add("-Xmn" +
Math.Floor(PageInstanceSetup.GetRam(ModInstanceList.McMcInstanceSelected,
!mcLaunchJavaSelected.Installation.Is64Bit) * 1024d * 0.15d) + "m");
var maxHeapArg = Math.Floor(PageInstanceSetup.GetRam(ModInstanceList.McMcInstanceSelected,
!mcLaunchJavaSelected.Installation.Is64Bit) * 1024d);
dataList.Add("-Xmx" + maxHeapArg + "m");
// #3282: 固定堆大小时追加 -Xms 使其等于 -Xmx(复用同一数值以保持一致),隐式禁用内存归还降低延迟抖动、利于 ZGC。
// 若 dataList 中已存在 -Xms(例如用户自定义参数已设)则跳过,避免重复/冲突。
if (Config.Launch.LockMemory && !dataList.Any(d => d.Contains("-Xms", StringComparison.OrdinalIgnoreCase)))
dataList.Add("-Xms" + maxHeapArg + "m");
dataList.Add("\"-Djava.library.path=" + GetNativesFolder() + "\"");
dataList.Add("-cp ${classpath}"); // 把支持库添加进启动参数表
// Authlib-Injector
if (mcLoginLoader.output.Type == "Auth")
{
if (mcLaunchJavaSelected.Installation.MajorVersion >= 6)
dataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT"); // 信任系统根证书(Meloong-Git/#5252
var server = mcLoginAuthLoader.input.BaseUrl.Replace("/authserver", "");
try
{
var response = Requester.FetchString(server);
dataList.Insert(0,
"-javaagent:\"" + Path.Combine(ModBase.pathPure, "authlib-injector.jar") + "\"=" + server +
" -Dauthlibinjector.side=client" + " -Dauthlibinjector.yggdrasil.prefetched=" +
Convert.ToBase64String(Encoding.UTF8.GetBytes(response)));
}
catch (WebException ex)
{
throw new Exception(
Lang.Text("Minecraft.Launch.Error.CannotConnectAuthServerWithDetail", server ?? null) + ex.InnerException, ex);
}
catch (Exception ex)
{
throw new Exception(Lang.Text("Minecraft.Launch.Error.CannotConnectAuthServer", server ?? null), ex);
}
}
if (Config.Instance.UseDebugLof4j2Config[instance.PathIndie])
{
if (ModInstanceList.McMcInstanceSelected.releaseTime.Year >= 2017)
dataList.Insert(0, "-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractDebugLog4j2Config() + "\"");
else
dataList.Insert(0,
"-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractLegacyDebugLog4j2Config() + "\"");
}
// 渲染器
var renderer = 0;
var instanceRenderer = Config.Instance.Renderer[ModInstanceList.McMcInstanceSelected?.PathInstance];
if (instanceRenderer != 0)
renderer = instanceRenderer - 1;
else
renderer = Config.Launch.Renderer;
var mesaLoaderWindowsTargetFile =
Path.Combine(ModBase.pathPure, "mesa-loader-windows", mesaLoaderWindowsVersion, "Loader.jar");
if (renderer != 0)
dataList.Insert(0,
"-javaagent:\"" + mesaLoaderWindowsTargetFile + "\"=" +
(renderer == 1 ? "llvmpipe" : renderer == 2 ? "d3d12" : "zink"));
// 设置代理
if (Config.Instance.UseProxy[instance.PathIndie] && Config.Network.HttpProxy.Type.Equals(2) &&
!string.IsNullOrWhiteSpace(Config.Network.HttpProxy.CustomAddress))
try
{
var proxyAddress = new Uri(Config.Network.HttpProxy.CustomAddress);
dataList.Add(
$"-D{(proxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyHost={proxyAddress.AbsoluteUri}");
dataList.Add(
$"-D{(proxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyPort={proxyAddress.Port}");
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Minecraft.Launch.Error.Proxy"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Minecraft.Launch.Error.Proxy"));
}
// 添加 LegacyFix 相关参数
if (McLaunchNeedsLegacyFix(instance))
{
var legacyFixPath = Path.Combine(ModBase.pathPure, "legacyfix.jar");
dataList.Add("-javaagent:\"" + legacyFixPath + "\"");
// Beta 1.6 以前版本需要添加的参数
if (instance.releaseTime < new DateTime(2011, 5, 25))
{
dataList.Add("-Djava.util.Arrays.useLegacyMergeSort=true");
}
}
// 添加 Java Wrapper 作为主 Jar
if (ModBase.IsUtf8CodePage() && !Config.Launch.DisableJlw &&
!Config.Instance.DisableJlw[ModInstanceList.McMcInstanceSelected?.PathInstance])
{
if (mcLaunchJavaSelected.Installation.MajorVersion >= 9)
dataList.Add("--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED");
dataList.Add("-Doolloo.jlw.tmpdir=\"" + ModBase.pathPure.TrimEnd('\\') + "\"");
dataList.Add("-jar \"" + ExtractJavaWrapper() + "\"");
}
// 添加 MainClass
if (instance.JsonObject["mainClass"] is null) throw new Exception(Lang.Text("Minecraft.Launch.Error.MissingMainClass"));
dataList.Add((string)instance.JsonObject["mainClass"]);
return dataList.Join(" ");
}
private static string McLaunchArgumentsJvmNew(McInstance instance)
{
var dataList = new List<string>();
// 获取 Json 中的 DataList
var currentInstance = instance;
while (true)
{
if (currentInstance.JsonObject["arguments"] is not null &&
currentInstance.JsonObject["arguments"]["jvm"] is not null)
foreach (var subJson in currentInstance.JsonObject["arguments"]["jvm"].AsArray())
if (subJson.GetValueKind() == JsonValueKind.String)
{
// 字符串类型
dataList.Add(subJson.ToString());
}
// 非字符串类型
else if (ModLibrary.McJsonRuleCheck(subJson["rules"]))
{
// 满足准则
if (subJson["value"].GetValueKind() == JsonValueKind.String)
dataList.Add(subJson["value"].ToString());
else
foreach (var value in subJson["value"].AsArray())
dataList.Add(value.ToString());
}
if (string.IsNullOrEmpty(currentInstance.InheritInstanceName))
break;
currentInstance = new McInstance(currentInstance.InheritInstanceName);
}
// 内存、Log4j 防御参数等
SecretLaunchJvmArgs(ref dataList);
// Authlib-Injector
if (mcLoginLoader.output.Type == "Auth")
{
if (mcLaunchJavaSelected.Installation.MajorVersion >= 6)
dataList.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT"); // 信任系统根证书(Meloong-Git/#5252
var server = mcLoginAuthLoader.input.BaseUrl.Replace("/authserver", "");
try
{
var response = ModNet.NetGetCodeByRequestRetry(server, Encoding.UTF8)?.ToString();
dataList.Insert(0,
"-javaagent:\"" + Path.Combine(ModBase.pathPure, "authlib-injector.jar") + "\"=" + server +
" -Dauthlibinjector.side=client" + " -Dauthlibinjector.yggdrasil.prefetched=" +
Convert.ToBase64String(Encoding.UTF8.GetBytes(response)));
}
catch (Exception ex)
{
throw new Exception(Lang.Text("Minecraft.Launch.Error.CannotConnectAuthServer", server ?? null), ex);
}
}
// LWJGL Unsafe Agent
if (McLaunchUsesLwjglUnsafeAgent(ModInstanceList.McMcInstanceSelected))
{
ModBase.Log($"获取到的 LWJGL 版本:{McLaunchGetLwjglVersion(ModInstanceList.McMcInstanceSelected)}");
dataList.Insert(0, $"-javaagent:\"{ModBase.pathPure}lwjgl-unsafe-agent.jar\"");
}
if (Config.Instance.UseDebugLof4j2Config[instance.PathIndie])
{
if (ModInstanceList.McMcInstanceSelected.releaseTime.Year >= 2017)
dataList.Insert(0, "-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractDebugLog4j2Config() + "\"");
else
dataList.Insert(0,
"-Dlog4j.configurationFile=\"" + LaunchEnvUtils.ExtractLegacyDebugLog4j2Config() + "\"");
}
// 渲染器
var renderer = 0;
var instanceRenderer = Config.Instance.Renderer[ModInstanceList.McMcInstanceSelected?.PathInstance];
if (instanceRenderer != 0)
renderer = instanceRenderer - 1;
else
renderer = Config.Launch.Renderer;
var mesaLoaderWindowsTargetFile =
Path.Combine(ModBase.pathPure, "mesa-loader-windows", mesaLoaderWindowsVersion, "Loader.jar");
if (renderer != 0)
dataList.Insert(0,
"-javaagent:\"" + mesaLoaderWindowsTargetFile + "\"=" +
(renderer == 1 ? "llvmpipe" : renderer == 2 ? "d3d12" : "zink"));
// 设置代理
if (Config.Instance.UseProxy[instance.PathIndie] && Config.Network.HttpProxy.Type.Equals(2) &&
!string.IsNullOrWhiteSpace(Config.Network.HttpProxy.CustomAddress))
try
{
var proxyAddress = new Uri(Config.Network.HttpProxy.CustomAddress);
dataList.Add(
$"-D{(proxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyHost={proxyAddress.AbsoluteUri}");
dataList.Add(
$"-D{(proxyAddress.Scheme.StartsWithF("https:") ? "https" : "http")}.proxyPort={proxyAddress.Port}");
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Minecraft.Launch.Error.Proxy"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Minecraft.Launch.Error.Proxy"));
}
// 添加 Java Wrapper 作为主 Jar
if (ModBase.IsUtf8CodePage() && !Config.Launch.DisableJlw &&
!Config.Instance.DisableJlw[ModInstanceList.McMcInstanceSelected?.PathInstance])
{
if (mcLaunchJavaSelected.Installation.MajorVersion >= 9)
dataList.Add("--add-exports cpw.mods.bootstraplauncher/cpw.mods.bootstraplauncher=ALL-UNNAMED");
dataList.Add("-Doolloo.jlw.tmpdir=\"" + ModBase.pathPure.TrimEnd('\\') + "\"");
dataList.Add("-jar \"" + ExtractJavaWrapper() + "\"");
}
// 将 "-XXX" 与后面 "XXX" 合并到一起
// 如果不合并,会导致 Forge 1.17 启动无效,它有两个 --add-exports,进一步导致其中一个在后面被去重
var deDuplicateDataList = new List<string>();
for (int i = 0, loopTo = dataList.Count - 1; i <= loopTo; i++)
{
var currentEntry = dataList[i];
if (dataList[i].StartsWithF("-"))
while (i < dataList.Count - 1)
{
if (dataList[i + 1].StartsWithF("-")) break;
i += 1;
currentEntry += " " + dataList[i];
}
deDuplicateDataList.Add(currentEntry.Trim().Replace("McEmu= ", "McEmu="));
}
// #3511 的清理
deDuplicateDataList.Remove("-XX:MaxDirectMemorySize=256M");
// 去重
var result = deDuplicateDataList.Distinct().ToList().Join(" ");
// 添加 MainClass
if (instance.JsonObject["mainClass"] is null) throw new Exception(Lang.Text("Minecraft.Launch.Error.MissingMainClass"));
result += " " + instance.JsonObject["mainClass"];
return result;
}
// Game 部分(第二段)
private static string McLaunchArgumentsGameOld(McInstance version)
{
var dataList = new List<string>();
// 本地化 Minecraft 启动信息
var basicString = version.JsonObject["minecraftArguments"].ToString();
if (!basicString.Contains("--height"))
basicString += " --height ${resolution_height} --width ${resolution_width}";
dataList.Add(basicString);
var result = dataList.Join(" ");
// 特别改变 OptiFineTweaker
if ((version.Info.HasForge || version.Info.HasLiteLoader) && version.Info.HasOptiFine)
{
// 把 OptiFineForgeTweaker 放在最后,不然会导致崩溃!
if (result.Contains("--tweakClass optifine.OptiFineForgeTweaker"))
{
ModBase.Log("[Launch] 发现正确的 OptiFineForge TweakClass,目前参数:" + result);
result = result.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "")
.Replace("--tweakClass optifine.OptiFineForgeTweaker ", "") +
" --tweakClass optifine.OptiFineForgeTweaker";
}
if (result.Contains("--tweakClass optifine.OptiFineTweaker"))
{
ModBase.Log("[Launch] 发现错误的 OptiFineForge TweakClass,目前参数:" + result);
result = result.Replace(" --tweakClass optifine.OptiFineTweaker", "")
.Replace("--tweakClass optifine.OptiFineTweaker ", "") +
" --tweakClass optifine.OptiFineForgeTweaker";
try
{
ModBase.WriteFile(Path.Combine(version.PathInstance, version.Name + ".json"),
ModBase.ReadFile(Path.Combine(version.PathInstance, version.Name + ".json"))
.Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"));
}
catch (Exception ex)
{
ModBase.Log(ex, "替换 OptiFineForge TweakClass 失败");
}
}
}
return result;
}
private static string McLaunchArgumentsGameNew(McInstance instance)
{
string mcLaunchArgumentsGameNewRet = default;
var dataList = new List<string>();
// 获取 Json 中的 DataList
var currentInstance = instance;
while (true)
{
if (currentInstance.JsonObject["arguments"] is not null &&
currentInstance.JsonObject["arguments"]["game"] is not null)
foreach (var subJson in currentInstance.JsonObject["arguments"]["game"].AsArray())
if (subJson.GetValueKind() == JsonValueKind.String)
{
// 字符串类型
dataList.Add(subJson.ToString());
}
// 非字符串类型
else if (ModLibrary.McJsonRuleCheck(subJson["rules"]))
{
// 满足准则
if (subJson["value"].GetValueKind() == JsonValueKind.String)
dataList.Add(subJson["value"].ToString());
else
foreach (var value in subJson["value"].AsArray())
dataList.Add(value.ToString());
}
if (string.IsNullOrEmpty(currentInstance.InheritInstanceName))
break;
currentInstance = new McInstance(currentInstance.InheritInstanceName);
}
// 将 "-XXX" 与后面 "XXX" 合并到一起
// 如果不进行合并 Impact 会启动无效,它有两个 --tweakclass
var deDuplicateDataList = new List<string>();
for (int i = 0, loopTo = dataList.Count - 1; i <= loopTo; i++)
{
var currentEntry = dataList[i];
if (dataList[i].StartsWithF("-"))
while (i < dataList.Count - 1)
{
if (dataList[i + 1].StartsWithF("-")) break;
i += 1;
currentEntry += " " + dataList[i];
}
deDuplicateDataList.Add(currentEntry);
}
// 去重
mcLaunchArgumentsGameNewRet = deDuplicateDataList.Distinct().ToList().Join(" ");
// 特别改变 OptiFineTweaker
if ((instance.Info.HasForge || instance.Info.HasLiteLoader) && instance.Info.HasOptiFine)
{
// 把 OptiFineForgeTweaker 放在最后,不然会导致崩溃!
if (mcLaunchArgumentsGameNewRet.Contains("--tweakClass optifine.OptiFineForgeTweaker"))
{
ModBase.Log("[Launch] 发现正确的 OptiFineForge TweakClass,目前参数:" + mcLaunchArgumentsGameNewRet);
mcLaunchArgumentsGameNewRet =
mcLaunchArgumentsGameNewRet.Replace(" --tweakClass optifine.OptiFineForgeTweaker", "")
.Replace("--tweakClass optifine.OptiFineForgeTweaker ", "") +
" --tweakClass optifine.OptiFineForgeTweaker";
}
if (mcLaunchArgumentsGameNewRet.Contains("--tweakClass optifine.OptiFineTweaker"))
{
ModBase.Log("[Launch] 发现错误的 OptiFineForge TweakClass,目前参数:" + mcLaunchArgumentsGameNewRet);
mcLaunchArgumentsGameNewRet =
mcLaunchArgumentsGameNewRet.Replace(" --tweakClass optifine.OptiFineTweaker", "")
.Replace("--tweakClass optifine.OptiFineTweaker ", "") +
" --tweakClass optifine.OptiFineForgeTweaker";
try
{
ModBase.WriteFile(Path.Combine(instance.PathInstance, instance.Name + ".json"),
ModBase.ReadFile(Path.Combine(instance.PathInstance, instance.Name + ".json"))
.Replace("optifine.OptiFineTweaker", "optifine.OptiFineForgeTweaker"));
}
catch (Exception ex)
{
ModBase.Log(ex, "替换 OptiFineForge TweakClass 失败");
}
}
}
return mcLaunchArgumentsGameNewRet;
}
// 替换 Arguments
private static Dictionary<string, string> McLaunchArgumentsReplace(McInstance instance,
ref ModLoader.LoaderTask<string, List<ModLibrary.McLibToken>> loader)
{
var gameArguments = new Dictionary<string, string>();
// 基础参数
gameArguments.Add("${classpath_separator}", ";");
gameArguments.Add("${natives_directory}", ModBase.ShortenPath(GetNativesFolder()));
gameArguments.Add("${library_directory}", ModBase.ShortenPath(ModFolder.mcFolderSelected + "libraries"));
gameArguments.Add("${libraries_directory}", ModBase.ShortenPath(ModFolder.mcFolderSelected + "libraries"));
gameArguments.Add("${launcher_name}", "PCLCE");
gameArguments.Add("${launcher_version}", ModBase.versionCode.ToString());
gameArguments.Add("${version_name}", instance.Name);
var argumentInfo = Config.Instance.TypeInfo[ModInstanceList.McMcInstanceSelected?.PathInstance];
gameArguments.Add("${version_type}",
string.IsNullOrEmpty(argumentInfo)
? Config.Launch.TypeInfo
: argumentInfo);
gameArguments.Add("${game_directory}",
ModBase.ShortenPath(ModInstanceList.McMcInstanceSelected.PathIndie[..^1]));
gameArguments.Add("${assets_root}", ModBase.ShortenPath(ModFolder.mcFolderSelected + "assets"));
gameArguments.Add("${user_properties}", "{}");
gameArguments.Add("${auth_player_name}", mcLoginLoader.output.Name);
gameArguments.Add("${auth_uuid}", mcLoginLoader.output.Uuid);
gameArguments.Add("${auth_access_token}", mcLoginLoader.output.AccessToken);
gameArguments.Add("${access_token}", mcLoginLoader.output.AccessToken);
gameArguments.Add("${auth_session}", mcLoginLoader.output.AccessToken);
gameArguments.Add("${user_type}", "msa"); // #1221
// 窗口尺寸参数
Size gameSize;
switch (Config.Launch.GameWindowMode)
{
case GameWindowSizeMode.Launcher: // 与启动器尺寸一致
{
Size result;
ModBase.RunInUiWait(() => result = new Size(ModBase.GetPixelSize(ModMain.frmMain.PanForm.ActualWidth),
ModBase.GetPixelSize(ModMain.frmMain.PanForm.ActualHeight)));
gameSize = result;
gameSize.Height -= 29.5d * ModBase.dpi / 96d; // 标题栏高度
break;
}
case GameWindowSizeMode.Custom: // 自定义
{
gameSize = new Size(Math.Max(100, (double)Config.Launch.GameWindowWidth),
Math.Max(100, (double)Config.Launch.GameWindowHeight));
break;
}
default:
{
gameSize = new Size(854d, 480d);
break;
}
}
if (ModInstanceList.McMcInstanceSelected.Info.Drop <= 120 && mcLaunchJavaSelected.Installation.MajorVersion <= 8 &&
mcLaunchJavaSelected.Installation.Version.Revision >= 200 &&
mcLaunchJavaSelected.Installation.Version.Revision <= 321 &&
!ModInstanceList.McMcInstanceSelected.Info.HasOptiFine && !ModInstanceList.McMcInstanceSelected.Info.HasForge)
{
// 修复 #34631.12.2-JRE 8u200~321 下窗口大小为设置大小的 DPI% 倍
McLaunchLog($"已应用窗口大小过大修复({mcLaunchJavaSelected.Installation.Version.Revision}");
gameSize.Width /= ModBase.dpi / 96d;
gameSize.Height /= ModBase.dpi / 96d;
}
gameArguments.Add("${resolution_width}", Math.Round(gameSize.Width).ToString(CultureInfo.InvariantCulture));
gameArguments.Add("${resolution_height}", Math.Round(gameSize.Height).ToString(CultureInfo.InvariantCulture));
// Assets 相关参数
gameArguments.Add("${game_assets}",
ModBase.ShortenPath(ModFolder.mcFolderSelected +
@"assets\virtual\legacy")); // 1.5.2 的 pre-1.6 资源索引应与 legacy 合并
gameArguments.Add("${assets_index_name}", ModAssets.McAssetsGetIndexName(instance));
// 支持库参数
var libList = ModLibrary.McLibListGet(instance, true);
loader.output = libList;
var cpStrings = new List<string>();
string optiFineCp = null;
// LegacyFix 释放
if (McLaunchNeedsLegacyFix(instance))
{
var legacyFixPath = Path.Combine(ModBase.pathPure, "legacyfix.jar");
try
{
ModBase.WriteFile(legacyFixPath, ModBase.GetResourceStream("Resources/legacyfix.jar"));
}
catch (Exception ex)
{
ModBase.Log(ex, "LegacyFix 释放失败");
}
}
// LWJGL Unsafe Agent 释放
if (McLaunchUsesLwjglUnsafeAgent(instance))
{
string agentPath = Path.Combine(ModBase.pathPure, "lwjgl-unsafe-agent.jar");
try
{
ModBase.WriteFile(agentPath, ModBase.GetResourceStream("Resources/lwjgl-unsafe-agent.jar"));
cpStrings.Add(agentPath);
}
catch (Exception ex)
{
ModBase.Log(ex, "LWJGL Unsafe Agent 释放失败");
}
}
foreach (var library in libList)
{
if (library.IsNatives)
continue;
if (ModInstanceList.McMcInstanceSelected.Info.HasCleanroom
&& library.OriginalName is not null
&& (library.OriginalName.Contains("org.lwjgl.lwjgl:lwjgl:2.9.4")
|| library.OriginalName.Contains("net.java.dev.jna:platform:3.4.0")
|| library.OriginalName.Contains("com.ibm.icu:icu4j-core-mojang:51.2")))
continue;
if (library.Name is not null && library.Name == "optifine:OptiFine")
optiFineCp = library.LocalPath;
else
cpStrings.Add(library.LocalPath);
}
foreach (var library in Config.Instance.ClasspathHead[instance.PathInstance].Split(";")) // 自定义 Classpath 头部
{
if (string.IsNullOrWhiteSpace(library))
continue;
cpStrings.Insert(0, library);
}
if (optiFineCp is not null)
cpStrings.Insert(cpStrings.Count - 2, optiFineCp); // OptiFine 的总是需要放到倒数第二位
gameArguments.Add("${classpath}", cpStrings.Select(c => ModBase.ShortenPath(c)).Join(";"));
return gameArguments;
}
#endregion
#region 解压 Natives
private static void McLaunchNatives(ModLoader.LoaderTask<List<ModLibrary.McLibToken>, int> loader)
{
// 创建文件夹
var target = GetNativesFolder() + @"\";
Directory.CreateDirectory(target);
// 解压文件
McLaunchLog("正在解压 Natives 文件");
var existFiles = new List<string>();
foreach (var native in loader.input)
{
if (!native.IsNatives)
continue;
ZipArchive zip;
try
{
zip = new ZipArchive(new FileStream(native.LocalPath, FileMode.Open));
}
catch (InvalidDataException ex)
{
ModBase.Log(ex, "打开 Natives 文件失败(" + native.LocalPath + "");
File.Delete(native.LocalPath);
throw new Exception(Lang.Text("Minecraft.Launch.Error.NativesCorrupted", native.LocalPath));
}
foreach (var entry in zip.Entries)
{
var fileName = entry.FullName;
if (fileName.EndsWithF(".dll", true))
{
// 实际解压文件的步骤
var filePath = target + fileName;
existFiles.Add(filePath);
var originalFile = new FileInfo(filePath);
if (originalFile.Exists)
{
if (originalFile.Length == entry.Length)
{
if (ModBase.modeDebug)
McLaunchLog("无需解压:" + filePath);
continue;
}
// 删除原文件
try
{
File.Delete(filePath);
}
catch (UnauthorizedAccessException ex)
{
McLaunchLog("删除原 dll 访问被拒绝,这通常代表有一个 MC 正在运行,跳过解压:" + filePath);
McLaunchLog("实际的错误信息:" + ex);
break;
}
}
// 解压新文件
ModBase.WriteFile(filePath, entry.Open());
McLaunchLog("已解压:" + filePath);
}
}
if (zip is not null)
zip.Dispose();
}
// 删除多余文件
foreach (var fileName in Directory.GetFiles(target))
{
if (existFiles.Contains(fileName))
continue;
try
{
McLaunchLog("删除:" + fileName);
File.Delete(fileName);
}
catch (UnauthorizedAccessException ex)
{
McLaunchLog("删除多余文件访问被拒绝,跳过删除步骤");
McLaunchLog("实际的错误信息:" + ex);
return;
}
}
}
/// <summary>
/// 获取 Natives 文件夹路径,不以 \ 结尾。
/// </summary>
private static string GetNativesFolder()
{
var result = Path.Combine(ModInstanceList.McMcInstanceSelected.PathInstance, ModInstanceList.McMcInstanceSelected.Name + "-natives");
if (SystemInfo.IsGBKEncoding || result.IsASCII())
return result;
result = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".minecraft", "bin", "natives");
if (result.IsASCII())
return result;
return Path.Combine(SystemPaths.DriveLetter, "ProgramData", "PCL", "natives");
}
#endregion
#region 启动与前后处理
private static void McLaunchPrerun()
{
// 要求 Java 使用高性能显卡
var javaExePath = mcLaunchJavaSelected.Installation.JavawExePath ??
mcLaunchJavaSelected.Installation.JavaExePath;
try
{
ModMain.SetGPUPreference(javaExePath, Config.Launch.SetGpuPreference);
}
catch (Exception ex)
{
if (ProcessInterop.IsAdmin() || !Config.Launch.SetGpuPreference)
{
ModBase.Log(ex, "直接调整显卡设置失败");
}
else
{
ModBase.Log(ex, "直接调整显卡设置失败,将以管理员权限重启 PCL 再次尝试");
try
{
if (ProcessInterop.StartAsAdmin($"--gpu \"{javaExePath}\"").ExitCode ==
(int)ModBase.ProcessReturnValues.TaskDone)
McLaunchLog("以管理员权限重启 PCL 并调整显卡设置成功");
else
throw new Exception("调整过程中出现异常");
}
catch (Exception exx)
{
ModBase.Log(
exx,
Lang.Text("Minecraft.Launch.Error.GpuSet"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Minecraft.Launch.Error.GpuSet"));
}
}
}
// 更新 launcher_profiles.json
do
{
try
{
// 确保可用
if (mcLoginLoader.output.Type != "Microsoft")
break;
ModFolder.McFolderLauncherProfilesJsonCreate(ModFolder.mcFolderSelected);
// 构建需要替换的 Json 对象
var replaceJsonString = @"
{
""authenticationDatabase"": {
""00000111112222233333444445555566"": {
""username"": """ + mcLoginLoader.output.Name.Replace("\"", "-") + @""",
""profiles"": {
""66666555554444433333222221111100"": {
""displayName"": """ + mcLoginLoader.output.Name + @"""
}
}
}
},
""clientToken"": """ + mcLoginLoader.output.ClientToken + @""",
""selectedUser"": {
""account"": ""00000111112222233333444445555566"",
""profile"": ""66666555554444433333222221111100""
}
}";
var replaceJson = (JsonObject)ModBase.GetJson(replaceJsonString);
// 更新文件
var profiles =
(JsonObject)ModBase.GetJson(
ModBase.ReadFile(ModFolder.mcFolderSelected + "launcher_profiles.json"));
profiles.Merge(replaceJson);
ModBase.WriteFile(ModFolder.mcFolderSelected + "launcher_profiles.json", profiles.ToString(),
encoding: Encoding.GetEncoding("GB18030"));
McLaunchLog("已更新 launcher_profiles.json");
}
catch (Exception ex)
{
ModBase.Log(ex, "更新 launcher_profiles.json 失败,将在删除文件后重试");
try
{
File.Delete(ModFolder.mcFolderSelected + "launcher_profiles.json");
ModFolder.McFolderLauncherProfilesJsonCreate(ModFolder.mcFolderSelected);
// 构建需要替换的 Json 对象
var replaceJsonString = @"
{
""authenticationDatabase"": {
""00000111112222233333444445555566"": {
""username"": """ + mcLoginLoader.output.Name.Replace("\"", "-") + @""",
""profiles"": {
""66666555554444433333222221111100"": {
""displayName"": """ + mcLoginLoader.output.Name + @"""
}
}
}
},
""clientToken"": """ + mcLoginLoader.output.ClientToken + @""",
""selectedUser"": {
""account"": ""00000111112222233333444445555566"",
""profile"": ""66666555554444433333222221111100""
}
}";
var replaceJson = (JsonObject)ModBase.GetJson(replaceJsonString);
// 更新文件
var profiles =
(JsonObject)ModBase.GetJson(
ModBase.ReadFile(ModFolder.mcFolderSelected + "launcher_profiles.json"));
profiles.Merge(replaceJson);
ModBase.WriteFile(ModFolder.mcFolderSelected + "launcher_profiles.json", profiles.ToString(),
encoding: Encoding.GetEncoding("GB18030"));
McLaunchLog("已在删除后更新 launcher_profiles.json");
}
catch (Exception exx)
{
ModBase.Log(
exx,
"更新 launcher_profiles.json 失败",
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Minecraft.Launch.Error.UpdateProfilesFailed"));
}
}
} while (false);
// 更新 options.txt
var setupFileAddress = Path.Combine(ModInstanceList.McMcInstanceSelected.PathIndie, "options.txt");
// 辅助切换游戏语言
if (Config.Tool.AutoChangeLanguage)
{
if (!File.Exists(setupFileAddress))
{
// Yosbr Mod 兼容(#2385):https://www.curseforge.com/minecraft/mc-mods/yosbr
var yosbrFileAddress = Path.Combine(ModInstanceList.McMcInstanceSelected.PathIndie, "config", "yosbr", "options.txt");
if (File.Exists(yosbrFileAddress))
{
McLaunchLog("将修改 Yosbr Mod 中的 options.txt");
setupFileAddress = yosbrFileAddress;
ModBase.WriteIni(setupFileAddress, "lang", "none"); // 忽略默认语言
}
}
try
{
// 语言
// 1.0- :没有语言选项
// 1.1 ~ 5 zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
// 1.6 ~ 10 zh_CN 时正常,zh_cn 时自动切换为英文
// 1.11 ~ 12zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
// 1.13+ zh_cn 时正常,zh_CN 时自动切换为英文
var currentLang = ModBase.ReadIni(setupFileAddress, "lang", "none");
var isLanguageUnconfigured = string.Equals(currentLang, "none", StringComparison.OrdinalIgnoreCase);
var hasExistingSaves = Directory.Exists(Path.Combine(ModInstanceList.McMcInstanceSelected.PathIndie, "saves"));
var shouldUseDefault = isLanguageUnconfigured || !hasExistingSaves;
var requiredLang = _ResolveMinecraftLanguage(currentLang, shouldUseDefault,
ModInstanceList.McMcInstanceSelected.releaseTime);
if (currentLang == requiredLang)
{
McLaunchLog($"需要的语言为 {requiredLang},当前语言为 {currentLang},无需修改");
}
else
{
ModBase.WriteIni(setupFileAddress, "lang", "-"); // 触发缓存更改,避免删除后重新下载残留缓存
ModBase.WriteIni(setupFileAddress, "lang", requiredLang);
McLaunchLog($"已将语言从 {currentLang} 修改为 {requiredLang}");
}
// 如果是初次设置,一并按启动器语言需要修改 forceUnicodeFont,确保 CJK 字符正常显示
if ((isLanguageUnconfigured || !hasExistingSaves) && _ShouldEnableForceUnicodeFont())
{
ModBase.WriteIni(setupFileAddress, "forceUnicodeFont", "true");
McLaunchLog("已开启 forceUnicodeFont,确保当前启动器语言字体正常显示");
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
"更新 options.txt 失败",
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Minecraft.Launch.Error.UpdateOptionsFailed"));
}
}
// 窗口
switch (Config.Launch.GameWindowMode)
{
case GameWindowSizeMode.Fullscreen: // 全屏
{
ModBase.WriteIni(setupFileAddress, "fullscreen", "true");
break;
}
case GameWindowSizeMode.Default: // 默认
// 其他
{
break;
}
default:
{
ModBase.WriteIni(setupFileAddress, "fullscreen", "false");
break;
}
}
}
private static string _ResolveMinecraftLanguage(string? currentLanguage, bool shouldUseLauncherLanguage,
DateTime? mcReleaseTime)
{
if (_IsMinecraftVersionUnder1Dot1(mcReleaseTime)) return "none";
var useLegacyRegionCase = _ShouldUseLegacyMinecraftLanguageCode(mcReleaseTime);
var languageCode = shouldUseLauncherLanguage
? LocalizationService.CurrentLanguage.Code
: currentLanguage;
return _NormalizeMinecraftLanguageCode(languageCode, useLegacyRegionCase);
}
private static string _NormalizeMinecraftLanguageCode(string? languageCode, bool useLegacyRegionCase)
{
var normalizedCode = string.IsNullOrWhiteSpace(languageCode)
? "none"
: languageCode.Replace('-', '_').Trim();
if (string.Equals(normalizedCode, "none", StringComparison.OrdinalIgnoreCase)) return "none";
var segments = normalizedCode.Split('_', 2, StringSplitOptions.RemoveEmptyEntries);
if (segments.Length < 2) return normalizedCode.ToLowerInvariant();
var language = segments[0].ToLowerInvariant();
var region = useLegacyRegionCase ? segments[1].ToUpperInvariant() : segments[1].ToLowerInvariant();
return $"{language}_{region}";
}
private static bool _IsMinecraftVersionUnder1Dot1(DateTime? releaseTime)
{
return releaseTime.HasValue &&
releaseTime.Value > new DateTime(2000, 1, 1) &&
releaseTime.Value <= new DateTime(2011, 11, 18);
}
private static bool _ShouldUseLegacyMinecraftLanguageCode(DateTime? releaseTime)
{
return releaseTime.HasValue &&
releaseTime.Value >= new DateTime(2012, 1, 12) &&
releaseTime.Value <= new DateTime(2016, 6, 8);
}
private static bool _ShouldEnableForceUnicodeFont()
{
return LocalizationService.CurrentLanguage.FontProfile is LocalizationFontProfile.SimplifiedChinese
or LocalizationFontProfile.TraditionalChinese
or LocalizationFontProfile.Japanese
or LocalizationFontProfile.Korean;
}
private static void McLaunchCustom(ModLoader.LoaderTask<int, int> loader)
{
// 获取自定义命令
var customCommandGlobal = Config.Launch.PreLaunchCommand;
if (!string.IsNullOrEmpty(customCommandGlobal))
customCommandGlobal = ArgumentReplace(customCommandGlobal, true);
var customCommandVersion = Config.Instance.PreLaunchCommand[ModInstanceList.McMcInstanceSelected?.PathInstance];
if (!string.IsNullOrEmpty(customCommandVersion))
customCommandVersion = ArgumentReplace(customCommandVersion, true);
// 输出 bat
try
{
var cmdString =
$"{(mcLaunchJavaSelected.Installation.MajorVersion > 8 ? "chcp 65001>nul" + "\r\n" : "")}" +
"@echo off" + "\r\n" + $"title 启动 - {ModInstanceList.McMcInstanceSelected.Name}" +
"\r\n" + "echo 游戏正在启动,请稍候。" + "\r\n" +
$"cd /D \"{ModBase.ShortenPath(ModInstanceList.McMcInstanceSelected.PathIndie)}\"" + "\r\n" +
customCommandGlobal + "\r\n" + customCommandVersion + "\r\n" +
$"\"{mcLaunchJavaSelected.Installation.JavaExePath}\" {mcLaunchArgument}" + "\r\n" +
"echo 游戏已退出。" + "\r\n" + "pause";
ModBase.WriteFile(currentLaunchOptions.SaveBatch ?? ModBase.exePath + @"PCL\LatestLaunch.bat",
McLogFilter.FilterAccessToken(cmdString, 'F'),
encoding: mcLaunchJavaSelected.Installation.MajorVersion > 8 ? Encoding.UTF8 : Encoding.Default);
if (currentLaunchOptions.SaveBatch is not null)
{
McLaunchLog("导出启动脚本完成,强制结束启动过程");
abortHint = Lang.Text("Minecraft.Launch.ExportScript.Success");
ModBase.OpenExplorer(currentLaunchOptions.SaveBatch);
loader.parent.Abort();
return; // 导出脚本完成
}
}
catch (Exception ex)
{
ModBase.Log(ex, "输出启动脚本失败");
if (currentLaunchOptions.SaveBatch is not null)
throw; // 直接触发启动失败
}
// 执行自定义命令
if (!string.IsNullOrEmpty(customCommandGlobal))
{
McLaunchLog("正在执行全局自定义命令:" + customCommandGlobal);
var customProcess = new Process();
try
{
customProcess.StartInfo.FileName = "cmd.exe";
customProcess.StartInfo.Arguments = "/c \"" + customCommandGlobal + "\"";
customProcess.StartInfo.WorkingDirectory = ModBase.ShortenPath(ModFolder.mcFolderSelected);
customProcess.StartInfo.UseShellExecute = false;
customProcess.StartInfo.CreateNoWindow = true;
customProcess.Start();
if (Config.Launch.PreLaunchCommandWait)
while (!customProcess.HasExited && !loader.IsAborted)
Thread.Sleep(10);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Minecraft.Launch.Error.CustomCommand"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Minecraft.Launch.Error.CustomCommand"));
}
finally
{
if (!customProcess.HasExited && loader.IsAborted)
{
McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程"); // #1183
customProcess.Kill();
}
}
}
if (!string.IsNullOrEmpty(customCommandVersion))
{
McLaunchLog("正在执行实例自定义命令:" + customCommandVersion);
var customProcess = new Process();
try
{
customProcess.StartInfo.FileName = "cmd.exe";
customProcess.StartInfo.Arguments = "/c \"" + customCommandVersion + "\"";
customProcess.StartInfo.WorkingDirectory = ModBase.ShortenPath(ModFolder.mcFolderSelected);
customProcess.StartInfo.UseShellExecute = false;
customProcess.StartInfo.CreateNoWindow = true;
customProcess.Start();
if (Config.Instance.PreLaunchCommandWait[ModInstanceList.McMcInstanceSelected?.PathInstance])
while (!customProcess.HasExited && !loader.IsAborted)
Thread.Sleep(10);
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Minecraft.Launch.Error.CustomCommand"),
ModBase.LogLevel.Hint,
userSummary: Lang.Text("Minecraft.Launch.Error.CustomCommand"));
}
finally
{
if (!customProcess.HasExited && loader.IsAborted)
{
McLaunchLog("由于取消启动,已强制结束自定义命令 CMD 进程"); // #1183
customProcess.Kill();
}
}
}
}
private static void McLaunchRun(ModLoader.LoaderTask<int, Process> loader)
{
var noJavaw = Config.Launch.NoJavaw &&
mcLaunchJavaSelected.Installation.JavawExePath is not null;
// 启动信息
var gameProcess = new Process();
var startInfo = new ProcessStartInfo(noJavaw
? mcLaunchJavaSelected.Installation.JavaExePath
: mcLaunchJavaSelected.Installation.JavawExePath);
// 设置环境变量
var paths = new List<string>(startInfo.EnvironmentVariables["Path"].Split(";"));
paths.Add(ModBase.ShortenPath(mcLaunchJavaSelected.Installation.JavaFolder));
startInfo.EnvironmentVariables["Path"] = paths.Distinct().ToList().Join(";");
startInfo.EnvironmentVariables["appdata"] = ModBase.ShortenPath(ModFolder.mcFolderSelected);
// 设置其他参数
startInfo.WorkingDirectory = ModBase.ShortenPath(ModInstanceList.McMcInstanceSelected.PathIndie);
startInfo.UseShellExecute = false;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardError = true;
startInfo.CreateNoWindow = noJavaw;
startInfo.Arguments = mcLaunchArgument;
gameProcess.StartInfo = startInfo;
// 开始进程
gameProcess.Start();
McLaunchLog("已启动游戏进程:" + startInfo.FileName);
if (loader.IsAborted)
{
McLaunchLog("由于取消启动,已强制结束游戏进程"); // #1631
gameProcess.Kill();
return;
}
loader.output = gameProcess;
mcLaunchProcess = gameProcess;
// 进程优先级处理
try
{
gameProcess.PriorityBoostEnabled = true;
switch (Config.Launch.ProcessPriority)
{
case GameProcessPriority.RealTime: // 实时
{
gameProcess.PriorityClass = ProcessPriorityClass.RealTime;
break;
}
case GameProcessPriority.High: // 极高
{
gameProcess.PriorityClass = ProcessPriorityClass.High;
break;
}
case GameProcessPriority.AboveNormal: // 高
{
gameProcess.PriorityClass = ProcessPriorityClass.AboveNormal;
break;
}
case GameProcessPriority.BelowNormal: // 低
{
gameProcess.PriorityClass = ProcessPriorityClass.BelowNormal;
break;
}
}
}
catch (Exception ex)
{
ModBase.Log(
ex,
Lang.Text("Minecraft.Launch.Error.PrioritySet"),
ModBase.LogLevel.Feedback,
userSummary: Lang.Text("Minecraft.Launch.Error.PrioritySet"));
}
}
private static void McLaunchWait(ModLoader.LoaderTask<Process, int> loader)
{
// 输出信息
McLaunchLog("");
McLaunchLog("~ 基础参数 ~");
McLaunchLog("PCL 版本:" + ModBase.versionBaseName + " (" + ModBase.versionCode + ")");
McLaunchLog(
$"游戏版本:{ModInstanceList.McMcInstanceSelected.Info.VanillaName}{ModInstanceList.McMcInstanceSelected.Info.vanilla}Drop {ModInstanceList.McMcInstanceSelected.Info.Drop}{(ModInstanceList.McMcInstanceSelected.Info.Reliable ? "" : ",无法完全确定")}");
McLaunchLog("资源版本:" + ModAssets.McAssetsGetIndexName(ModInstanceList.McMcInstanceSelected));
McLaunchLog("实例继承:" + (string.IsNullOrEmpty(ModInstanceList.McMcInstanceSelected.InheritInstanceName)
? "无"
: ModInstanceList.McMcInstanceSelected.InheritInstanceName));
var launchRamGb = PageInstanceSetup.GetRam(ModInstanceList.McMcInstanceSelected,
!mcLaunchJavaSelected.Installation.Is64Bit);
McLaunchLog("分配的内存:" +
launchRamGb.ToString("N1", CultureInfo.InvariantCulture) + " GiB" +
Math.Round(launchRamGb * 1024d).ToString("N0", CultureInfo.InvariantCulture) + " MiB");
McLaunchLog("MC 文件夹:" + ModFolder.mcFolderSelected);
McLaunchLog("实例文件夹:" + ModInstanceList.McMcInstanceSelected.PathInstance);
McLaunchLog("版本隔离:" + ((ModInstanceList.McMcInstanceSelected.PathIndie ?? "") ==
(ModInstanceList.McMcInstanceSelected.PathInstance ?? "")));
McLaunchLog("HMCL 格式:" + ModInstanceList.McMcInstanceSelected.IsHmclFormatJson);
McLaunchLog("Java 信息:" + mcLaunchJavaSelected.Installation);
// McLaunchLog("环境变量:" & If(McLaunchJavaSelected IsNot Nothing, If(McLaunchJavaSelected.HasEnvironment, "已设置", "未设置"), "未设置"))
McLaunchLog("Natives 文件夹:" + GetNativesFolder());
McLaunchLog("");
McLaunchLog("~ 档案参数 ~");
McLaunchLog("玩家用户名:" + mcLoginLoader.output.Name);
McLaunchLog("AccessToken" + mcLoginLoader.output.AccessToken);
McLaunchLog("ClientToken" + mcLoginLoader.output.ClientToken);
McLaunchLog("UUID" + mcLoginLoader.output.Uuid);
McLaunchLog("验证方式:" + mcLoginLoader.output.Type);
McLaunchLog("");
// 获取窗口标题
var windowTitle = Config.Instance.Title[ModInstanceList.McMcInstanceSelected?.PathInstance];
if (string.IsNullOrEmpty(windowTitle) &&
!Config.Instance.UseGlobalTitle[ModInstanceList.McMcInstanceSelected?.PathInstance])
windowTitle = Config.Launch.Title;
windowTitle = ArgumentReplace(windowTitle, false);
// JStack 路径
var jStackPath = Path.Combine(mcLaunchJavaSelected.Installation.JavaFolder, "jstack.exe");
// 初始化等待
var watcher = new ModWatcher.Watcher(loader, ModInstanceList.McMcInstanceSelected, windowTitle,
File.Exists(jStackPath) ? jStackPath : "", currentLaunchOptions.IsTest);
mcLaunchWatcher = watcher;
// 显示实时日志
if (currentLaunchOptions.IsTest)
{
if (ModMain.frmLogLeft is null)
ModBase.RunInUiWait(() => ModMain.frmLogLeft = new PageLogLeft());
if (ModMain.frmLogRight is null)
ModBase.RunInUiWait(() =>
{
ModAnimation.AniControlEnabled += 1;
ModMain.frmLogRight = new PageLogRight();
ModAnimation.AniControlEnabled -= 1;
});
ModMain.frmLogLeft.Add(watcher);
McLaunchLog("已显示游戏实时日志");
}
// 等待
while (watcher.State == ModWatcher.Watcher.MinecraftState.Loading)
Thread.Sleep(100);
if (watcher.State == ModWatcher.Watcher.MinecraftState.Crashed) throw new Exception("$$");
}
private static void McLaunchEnd()
{
McLaunchLog("开始启动结束处理");
// 暂停或开始音乐播放
if (Config.Preference.Music.StopInGame)
ModBase.RunInUi(() =>
{
if (ModMusic.MusicPause()) ModBase.Log("[Music] 已根据设置,在启动后暂停音乐播放");
});
else if (Config.Preference.Music.StartInGame)
ModBase.RunInUi(() =>
{
if (ModMusic.MusicResume()) ModBase.Log("[Music] 已根据设置,在启动后开始音乐播放");
});
// 暂停视频背景播放
ModVideoBack.IsGaming = true;
ModVideoBack.VideoPause();
// 启动器可见性
McLaunchLog(
"启动器可见性:" + Config.Launch.LauncherVisibility);
switch (Config.Launch.LauncherVisibility)
{
case LauncherVisibility.ExitImmediately:
{
// 直接关闭
McLaunchLog("已根据设置,在启动后关闭启动器");
ModBase.RunInUi(() => ModMain.frmMain.EndProgram(false));
break;
}
case LauncherVisibility.HideAndExit:
case LauncherVisibility.HideAndReopen:
{
// 隐藏
McLaunchLog("已根据设置,在启动后隐藏启动器");
ModBase.RunInUi(() => ModMain.frmMain.Hidden = true);
break;
}
case LauncherVisibility.MinimizeAndReopen:
{
// 最小化
McLaunchLog("已根据设置,在启动后最小化启动器");
ModBase.RunInUi(() => ModMain.frmMain.WindowState = WindowState.Minimized);
break;
}
case LauncherVisibility.DoNothing:
{
break;
}
// 啥都不干
}
// 启动计数
States.System.LaunchCount += 1;
States.Instance.LaunchCount[ModInstanceList.McMcInstanceSelected.PathInstance] =
States.Instance.LaunchCount[ModInstanceList.McMcInstanceSelected.PathInstance] + 1;
}
/// <summary>
/// 对替换标记进行处理。会对替换内容使用 EscapeHandler 进行转义。
/// </summary>
private static string ArgumentReplace(string text, bool replaceTime, Func<string, string> escapeHandler = null)
{
// 预处理
if (text is null)
return null;
string replacer(string s)
{
if (s is null)
return "";
if (escapeHandler is null)
return s;
if (s.Contains(@":\"))
s = ModBase.ShortenPath(s);
return escapeHandler(s);
}
;
// 基础
text = text.Replace("{pcl_version}", replacer(ModBase.versionBaseName));
text = text.Replace("{pcl_version_code}", replacer(ModBase.versionCode.ToString()));
text = text.Replace("{pcl_version_branch}", replacer(ModBase.versionBranchName));
text = text.Replace("{identify}", replacer(Identify.LauncherId));
text = text.Replace("{path}", replacer(Basics.CurrentDirectory));
text = text.Replace("{path_with_name}", replacer(Basics.ExecutablePath));
text = text.Replace("{path_temp}", replacer(ModBase.pathTemp));
// 时间
if (replaceTime) // 在窗口标题中,时间会被后续动态替换,所以此时不应该替换
{
text = text.Replace("{date}", replacer(Lang.Date(DateTime.Now, "d")));
text = text.Replace("{time}", replacer(Lang.Date(DateTime.Now, "T")));
}
// Minecraft
text = text.Replace("{java}", replacer(mcLaunchJavaSelected?.Installation.JavaFolder));
text = text.Replace("{minecraft}", replacer(ModFolder.mcFolderSelected));
if (ModInstanceList.McMcInstanceSelected?.IsLoaded == true)
{
text = text.Replace("{version_path}", replacer(ModInstanceList.McMcInstanceSelected.PathInstance));
text = text.Replace("{verpath}", replacer(ModInstanceList.McMcInstanceSelected.PathInstance));
text = text.Replace("{version_indie}", replacer(ModInstanceList.McMcInstanceSelected.PathIndie));
text = text.Replace("{verindie}", replacer(ModInstanceList.McMcInstanceSelected.PathIndie));
text = text.Replace("{name}", replacer(ModInstanceList.McMcInstanceSelected.Name));
if (new[] { "unknown", "old", "pending" }.Contains(
ModInstanceList.McMcInstanceSelected.Info.VanillaName.ToLower()))
text = text.Replace("{version}", replacer(ModInstanceList.McMcInstanceSelected.Name));
else
text = text.Replace("{version}", replacer(ModInstanceList.McMcInstanceSelected.Info.VanillaName));
}
else
{
text = text.Replace("{version_path}", replacer(null));
text = text.Replace("{verpath}", replacer(null));
text = text.Replace("{version_indie}", replacer(null));
text = text.Replace("{verindie}", replacer(null));
text = text.Replace("{name}", replacer(null));
text = text.Replace("{version}", replacer(null));
}
// 登录信息
if (mcLoginLoader.State == ModBase.LoadState.Finished)
{
text = text.Replace("{user}", replacer(mcLoginLoader.output.Name));
text = text.Replace("{uuid}", replacer(mcLoginLoader.output.Uuid?.ToLower()));
switch (mcLoginLoader.input.LoginType)
{
case McLoginType.Legacy:
{
text = text.Replace("{login}", replacer("离线"));
break;
}
case McLoginType.Ms:
{
text = text.Replace("{login}", replacer("正版"));
break;
}
case McLoginType.Auth:
{
text = text.Replace("{login}", replacer("Authlib-Injector"));
break;
}
}
}
else
{
text = text.Replace("{user}", replacer(null));
text = text.Replace("{uuid}", replacer(null));
text = text.Replace("{login}", replacer(null));
}
return text;
}
#endregion
}