feat: add guided login and authlib URL setup

This commit is contained in:
Anon 2026-07-26 14:17:59 +02:00
parent 47bc47786a
commit 630a5fabef
10 changed files with 528 additions and 66 deletions

View file

@ -58,6 +58,7 @@ namespace MinecraftClient
private static int offlinePromptActive;
private static int exitOnFailurePending;
private static string settingsIniPath = "MinecraftClient.ini";
private static AuthenticationSelection? pendingAuthenticationSelection;
// [SENTRY]
// Setting this string to an empty string will disable Sentry
@ -565,15 +566,19 @@ namespace MinecraftClient
// Setup exit cleaning code
ExitCleanUp.Add(() => { DoExit(); });
if (HasNoConfiguredLoginDetails())
{
if (!PromptForAuthenticationSelection())
return;
}
//Asking the user to type in missing data such as Username and Password
bool useBrowser = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.browser;
bool useDeviceCode = Config.Main.General.AccountType == LoginType.microsoft && Config.Main.General.Method == LoginMethod.mcc;
bool skipPassword = useBrowser || useDeviceCode;
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !useBrowser)
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login) && !skipPassword)
{
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login);
InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim();
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login))
if (!RequestLogin())
{
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
return;
@ -603,6 +608,145 @@ namespace MinecraftClient
InternalConfig.Account.Password = password;
}
private static bool HasNoConfiguredLoginDetails()
=> string.IsNullOrWhiteSpace(InternalConfig.Account.Login)
&& string.IsNullOrWhiteSpace(InternalConfig.Account.Password);
private static bool PromptForAuthenticationSelection()
{
while (true)
{
ConsoleIO.WriteLine(Translations.mcc_auth_method_prompt);
string selection = ConsoleIO.ReadLine().Trim();
switch (selection.ToLowerInvariant())
{
case "1":
case "offline":
BeginAuthenticationSelection(LoginType.mojang, LoginMethod.mcc);
if (!RequestLogin())
{
DiscardAuthenticationSelection();
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
return false;
}
InternalConfig.Account.Password = "-";
return true;
case "2":
case "online":
case "microsoft":
BeginAuthenticationSelection(LoginType.microsoft, LoginMethod.mcc);
return true;
case "3":
case "yggdrasil":
BeginAuthenticationSelection(LoginType.yggdrasil, LoginMethod.mcc);
if (!RequestLogin() || !RequestRequiredPassword())
{
DiscardAuthenticationSelection();
HandleFailure(Translations.error_login_blocked, false, ChatBot.DisconnectReason.LoginRejected);
return false;
}
if (!RequestAuthlibServer())
{
DiscardAuthenticationSelection();
return false;
}
return true;
default:
ConsoleIO.WriteLine(Translations.mcc_auth_method_invalid);
break;
}
}
}
private static void BeginAuthenticationSelection(LoginType accountType, LoginMethod method)
{
pendingAuthenticationSelection ??= new AuthenticationSelection(
Config.Main.General.AccountType,
Config.Main.General.Method,
Config.Main.General.AuthServerUrl);
Config.Main.General.AccountType = accountType;
Config.Main.General.Method = method;
}
private static bool RequestLogin()
{
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? Translations.mcc_login_basic_io : Translations.mcc_login);
InternalConfig.Account.Login = ConsoleIO.ReadLine().Trim();
return !string.IsNullOrWhiteSpace(InternalConfig.Account.Login);
}
private static bool RequestRequiredPassword()
{
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden);
string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword();
if (string.IsNullOrWhiteSpace(password))
return false;
InternalConfig.Account.Password = password;
return true;
}
private static bool RequestAuthlibServer()
{
while (true)
{
ConsoleIO.WriteLine(Translations.mcc_yggdrasil_url);
string authServerUrl = ConsoleIO.ReadLine().Trim();
if (!Config.Main.General.TrySetAuthServerUrl(authServerUrl)
|| !Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
{
ConsoleIO.WriteLine(Translations.mcc_yggdrasil_invalid_url);
continue;
}
switch (ProtocolHandler.ValidateAuthlibServer(authServerUri))
{
case ProtocolHandler.AuthlibServerValidationResult.Valid:
return true;
case ProtocolHandler.AuthlibServerValidationResult.Unreachable:
ConsoleIO.WriteLine(Translations.mcc_yggdrasil_server_unreachable);
break;
default:
ConsoleIO.WriteLine(Translations.mcc_yggdrasil_server_invalid);
break;
}
}
}
private static void PersistAuthenticationSelection(SessionToken session)
{
if (pendingAuthenticationSelection is null)
return;
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Login))
InternalConfig.Account.Login = session.PlayerName;
Config.Main.General.Account = InternalConfig.Account;
WriteBackSettings();
pendingAuthenticationSelection = null;
}
private static void DiscardAuthenticationSelection()
{
if (pendingAuthenticationSelection is not AuthenticationSelection selection)
return;
Config.Main.General.AccountType = selection.AccountType;
Config.Main.General.Method = selection.Method;
Config.Main.General.AuthServerUrl = selection.AuthServerUrl;
pendingAuthenticationSelection = null;
}
private sealed record AuthenticationSelection(LoginType AccountType, LoginMethod Method, string AuthServerUrl);
/// <summary>
/// Start a new Client
/// </summary>
@ -664,17 +808,26 @@ namespace MinecraftClient
if (result != ProtocolHandler.LoginResult.Success)
{
ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : (Config.Main.General.AccountType == LoginType.microsoft ? "Microsoft" : Config.Main.General.AuthServer.Host)));
ConsoleIO.WriteLine(string.Format(Translations.mcc_connecting, Config.Main.General.AccountType == LoginType.mojang ? "Minecraft.net" : (Config.Main.General.AccountType == LoginType.microsoft ? "Microsoft" : Config.Main.General.AuthServerUrl)));
result = ProtocolHandler.GetLogin(InternalConfig.Account.Login, InternalConfig.Account.Password, Config.Main.General.AccountType, out session);
}
if (result == ProtocolHandler.LoginResult.Success && Config.Main.Advanced.SessionCache != CacheType.none)
SessionCache.Store(loginLower, session);
if (result == ProtocolHandler.LoginResult.Success)
{
PersistAuthenticationSelection(session);
loginLower = ToLowerIfNeed(InternalConfig.Account.Login);
if (Config.Main.Advanced.SessionCache != CacheType.none)
SessionCache.Store(loginLower, session);
}
if (result == ProtocolHandler.LoginResult.Success)
session.SessionPreCheckTask = Task.Factory.StartNew(() => session.SessionPreCheck(Config.Main.General.AccountType));
}
if (result == ProtocolHandler.LoginResult.Success)
PersistAuthenticationSelection(session);
if (result == ProtocolHandler.LoginResult.Success)
{
InternalConfig.Username = session.PlayerName;
@ -859,6 +1012,7 @@ namespace MinecraftClient
}
else
{
DiscardAuthenticationSelection();
string failureMessage = Translations.error_login;
string failureReason = result switch
{

View file

@ -153,7 +153,7 @@ namespace MinecraftClient.Protocol
// Extract email from JWT id_token
string payload = JwtPayloadDecode.GetPayload(jsonData["id_token"]!.GetStringValue());
var jsonPayload = Json.ParseJson(payload);
string email = jsonPayload!["email"]!.GetStringValue();
string email = jsonPayload?["email"]?.GetStringValue() ?? string.Empty;
return new LoginResponse()
{
@ -195,7 +195,7 @@ namespace MinecraftClient.Protocol
// Extract email from JWT
string payload = JwtPayloadDecode.GetPayload(jsonData["id_token"]!.GetStringValue());
var jsonPayload = Json.ParseJson(payload);
string email = jsonPayload!["email"]!.GetStringValue();
string email = jsonPayload?["email"]?.GetStringValue() ?? string.Empty;
return new LoginResponse()
{
Email = email,

View file

@ -26,9 +26,10 @@ namespace MinecraftClient.Protocol.ProfileKey
ProxiedWebRequest.Response? response = null;
try
{
var authServer = Settings.Config.Main.General.AuthServer;
var request = new ProxiedWebRequest(
(authServer.UseHttps ? "https" : "http") + "://" + authServer.Host + ":" + authServer.Port + authServer.AuthlibInjectorAPIPath)
if (!Settings.Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
return false;
var request = new ProxiedWebRequest(authServerUri.AbsoluteUri)
{
Accept = "application/json"
};
@ -66,9 +67,10 @@ namespace MinecraftClient.Protocol.ProfileKey
string certificatesURL = "https://api.minecraftservices.com/player/certificates";
if (isYggdrasil)
{
var authServer = Settings.Config.Main.General.AuthServer;
certificatesURL = (authServer.UseHttps ? "https" : "http") + "://" + authServer.Host + ":" + authServer.Port +
authServer.AuthlibInjectorAPIPath + "/minecraftservices/player/certificates";
if (!Settings.Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
return null;
certificatesURL = new Uri(authServerUri, "minecraftservices/player/certificates").AbsoluteUri;
}
ProxiedWebRequest.Response? response = null;

View file

@ -27,6 +27,13 @@ namespace MinecraftClient.Protocol
/// </remarks>
public static class ProtocolHandler
{
public enum AuthlibServerValidationResult
{
Valid,
Unreachable,
InvalidResponse
}
/// <summary>
/// Perform a DNS lookup for a Minecraft Service using the specified domain name
/// </summary>
@ -134,6 +141,37 @@ namespace MinecraftClient.Protocol
}
}
/// <summary>
/// Verifies that an authlib-injector URL is reachable and returns its metadata document.
/// </summary>
public static AuthlibServerValidationResult ValidateAuthlibServer(Uri authServerUri)
{
string result = string.Empty;
int statusCode;
try
{
statusCode = DoHTTPSGet(authServerUri, ref result);
}
catch
{
return AuthlibServerValidationResult.Unreachable;
}
if (statusCode != 200)
return AuthlibServerValidationResult.InvalidResponse;
try
{
return Json.ParseJson(result)?["meta"]?["implementationName"]?.GetStringValue() is { Length: > 0 }
? AuthlibServerValidationResult.Valid
: AuthlibServerValidationResult.InvalidResponse;
}
catch
{
return AuthlibServerValidationResult.InvalidResponse;
}
}
/// <summary>
/// Get a protocol handler for the specified Minecraft version
/// </summary>
@ -714,9 +752,10 @@ namespace MinecraftClient.Protocol
string json_request = "{\"agent\": { \"name\": \"Minecraft\", \"version\": 1 }, \"username\": \"" +
JsonEncode(user) + "\", \"password\": \"" + JsonEncode(pass) +
"\", \"clientToken\": \"" + JsonEncode(session.ClientID) + "\" }";
int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port,
Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/authenticate", json_request,
Config.Main.General.AuthServer.UseHttps, ref result);
if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
return LoginResult.OtherError;
int code = DoHTTPSPost(authServerUri, "authserver/authenticate", json_request, ref result);
if (code == 200)
{
if (result.Contains("availableProfiles\":[]}"))
@ -922,7 +961,8 @@ namespace MinecraftClient.Protocol
session.PlayerID = profile.UUID;
session.ID = accessToken;
session.RefreshToken = msaResponse.RefreshToken;
InternalConfig.Account.Login = msaResponse.Email;
if (!string.IsNullOrWhiteSpace(msaResponse.Email))
InternalConfig.Account.Login = msaResponse.Email;
return LoginResult.Success;
}
else
@ -1037,9 +1077,10 @@ namespace MinecraftClient.Protocol
"\", \"clientToken\": \"" + JsonEncode(currentsession.ClientID) +
"\", \"selectedProfile\": { \"id\": \"" + JsonEncode(currentsession.PlayerID) +
"\", \"name\": \"" + JsonEncode(currentsession.PlayerName) + "\" } }";
int code = DoHTTPSPost(Config.Main.General.AuthServer.Host, Config.Main.General.AuthServer.Port,
Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/authserver/refresh", json_request,
Config.Main.General.AuthServer.UseHttps, ref result);
if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
return LoginResult.OtherError;
int code = DoHTTPSPost(authServerUri, "authserver/refresh", json_request, ref result);
if (code == 200)
{
if (result is null)
@ -1093,16 +1134,18 @@ namespace MinecraftClient.Protocol
string result = "";
string json_request = "{\"accessToken\":\"" + accesstoken + "\",\"selectedProfile\":\"" + uuid +
"\",\"serverId\":\"" + serverhash + "\"}";
string host = type == LoginType.yggdrasil
? Config.Main.General.AuthServer.Host
: "sessionserver.mojang.com";
int port = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.Port : 443;
string endpoint = type == LoginType.yggdrasil
? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join"
: "/session/minecraft/join";
int code;
if (type == LoginType.yggdrasil)
{
if (!Config.Main.General.TryGetAuthServerUri(out Uri? authServerUri))
return false;
bool useHttps = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.UseHttps : true;
int code = DoHTTPSPost(host, port, endpoint, json_request, useHttps, ref result);
code = DoHTTPSPost(authServerUri, "sessionserver/session/minecraft/join", json_request, ref result);
}
else
{
code = DoHTTPSPost("sessionserver.mojang.com", 443, "/session/minecraft/join", json_request, ref result);
}
return (code >= 200 && code < 300);
}
catch
@ -1241,6 +1284,16 @@ namespace MinecraftClient.Protocol
return DoHTTPSRequest(HttpMethod.Get, host, port, path, headers, null, useHttps: true, ref result);
}
private static int DoHTTPSGet(Uri requestUri, ref string result)
{
Dictionary<string, string> headers = new()
{
{ "User-Agent", "MCC/" + Program.Version }
};
return DoHTTPSRequest(HttpMethod.Get, requestUri.Host, requestUri.Port, requestUri.PathAndQuery, headers,
null, requestUri.Scheme == Uri.UriSchemeHttps, ref result);
}
/// <summary>
/// Make a POST request to the specified endpoint of the Mojang API
/// </summary>
@ -1253,6 +1306,13 @@ namespace MinecraftClient.Protocol
private static int DoHTTPSPost(string host, int port, string path, string body, ref string result)
=> DoHTTPSPost(host, port, path, body, useHttps: true, ref result);
private static int DoHTTPSPost(Uri baseUri, string relativePath, string body, ref string result)
{
Uri requestUri = new(baseUri, relativePath);
return DoHTTPSPost(requestUri.Host, requestUri.Port, requestUri.PathAndQuery, body,
requestUri.Scheme == Uri.UriSchemeHttps, ref result);
}
/// <summary>
/// Make a POST request to the specified endpoint of the Mojang API
/// </summary>
@ -1392,4 +1452,4 @@ namespace MinecraftClient.Protocol
return dateTime;
}
}
}
}

View file

@ -2011,6 +2011,15 @@ namespace MinecraftClient {
return ResourceManager.GetString("Main.General.AuthlibServer", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Authlib-injector URL to use for Yggdrasil accounts. It must use http or https and include any required path..
/// </summary>
internal static string Main_General_AuthServerUrl {
get {
return ResourceManager.GetString("Main.General.AuthServerUrl", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Yggdrasil authlib multi-user selection..

View file

@ -983,6 +983,9 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be
<data name="Main.General.AuthlibServer" xml:space="preserve">
<value>authlib-injector authentication server to use for Yggdrasil accounts</value>
</data>
<data name="Main.General.AuthServerUrl" xml:space="preserve">
<value>Authlib-injector URL to use for Yggdrasil accounts. It must use http or https and include any required path.</value>
</data>
<data name="AuthlibServer.Host" xml:space="preserve">
<value>Domain name or IP address</value>
</data>

View file

@ -5998,6 +5998,24 @@ namespace MinecraftClient {
return ResourceManager.GetString("mcc.connecting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Select a login method: [1] Offline, [2] Online (Microsoft), [3] Yggdrasil.
/// </summary>
internal static string mcc_auth_method_prompt {
get {
return ResourceManager.GetString("mcc.auth_method_prompt", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please choose 1, 2, or 3..
/// </summary>
internal static string mcc_auth_method_invalid {
get {
return ResourceManager.GetString("mcc.auth_method_invalid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tip: try TUI mode for a cleaner interface, mouse-friendly container actions, and a nicer layout. Run {0}feature tui§8 to switch [Console.General] ConsoleMode to &quot;tui&quot; for the next restart..
@ -6271,6 +6289,114 @@ namespace MinecraftClient {
return ResourceManager.GetString("mcc.password_hidden", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Authlib server host:.
/// </summary>
internal static string mcc_yggdrasil_host {
get {
return ResourceManager.GetString("mcc.yggdrasil_host", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Authlib server port [{0}]:.
/// </summary>
internal static string mcc_yggdrasil_port {
get {
return ResourceManager.GetString("mcc.yggdrasil_port", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Authlib-Injector API path [{0}]:.
/// </summary>
internal static string mcc_yggdrasil_api_path {
get {
return ResourceManager.GetString("mcc.yggdrasil_api_path", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use HTTPS? [Y/n].
/// </summary>
internal static string mcc_yggdrasil_use_https {
get {
return ResourceManager.GetString("mcc.yggdrasil_use_https", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The authlib server host cannot be empty..
/// </summary>
internal static string mcc_yggdrasil_invalid_host {
get {
return ResourceManager.GetString("mcc.yggdrasil_invalid_host", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The authlib server port must be between 1 and 65535..
/// </summary>
internal static string mcc_yggdrasil_invalid_port {
get {
return ResourceManager.GetString("mcc.yggdrasil_invalid_port", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Please answer yes or no..
/// </summary>
internal static string mcc_yggdrasil_invalid_yes_no {
get {
return ResourceManager.GetString("mcc.yggdrasil_invalid_yes_no", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Authlib-injector URL:.
/// </summary>
internal static string mcc_yggdrasil_url {
get {
return ResourceManager.GetString("mcc.yggdrasil_url", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The authlib-injector URL must be an absolute HTTP or HTTPS URL..
/// </summary>
internal static string mcc_yggdrasil_invalid_url {
get {
return ResourceManager.GetString("mcc.yggdrasil_invalid_url", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Could not reach the authlib-injector server. Check the URL and try again..
/// </summary>
internal static string mcc_yggdrasil_server_unreachable {
get {
return ResourceManager.GetString("mcc.yggdrasil_server_unreachable", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The server did not return valid authlib-injector metadata. Check the URL and try again..
/// </summary>
internal static string mcc_yggdrasil_server_invalid {
get {
return ResourceManager.GetString("mcc.yggdrasil_server_invalid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The configured authlib-injector URL must be an absolute HTTP or HTTPS URL..
/// </summary>
internal static string config_auth_server_url_invalid {
get {
return ResourceManager.GetString("config.auth_server_url_invalid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to You are dead. Type &apos;{0}respawn&apos; to respawn..

View file

@ -2004,6 +2004,27 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
<data name="mcc.connecting" xml:space="preserve">
<value>Connecting to {0}...</value>
</data>
<data name="mcc.auth_method_prompt" xml:space="preserve">
<value>Select a login method: [1] Offline, [2] Online (Microsoft), [3] Yggdrasil</value>
</data>
<data name="mcc.auth_method_invalid" xml:space="preserve">
<value>Please choose 1, 2, or 3.</value>
</data>
<data name="mcc.yggdrasil_url" xml:space="preserve">
<value>Authlib-injector URL:</value>
</data>
<data name="mcc.yggdrasil_invalid_url" xml:space="preserve">
<value>The authlib-injector URL must be an absolute HTTP or HTTPS URL.</value>
</data>
<data name="mcc.yggdrasil_server_unreachable" xml:space="preserve">
<value>Could not reach the authlib-injector server. Check the URL and try again.</value>
</data>
<data name="mcc.yggdrasil_server_invalid" xml:space="preserve">
<value>The server did not return valid authlib-injector metadata. Check the URL and try again.</value>
</data>
<data name="config.auth_server_url_invalid" xml:space="preserve">
<value>The configured authlib-injector URL must be an absolute HTTP or HTTPS URL.</value>
</data>
<data name="mcc.console_mode_tui_recommendation" xml:space="preserve">
<value>Tip: try TUI mode for a cleaner interface, mouse-friendly inventory actions, and a nicer layout. Run {0}tryout tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart.</value>
</data>
@ -2097,6 +2118,27 @@ Type '{0}quit' to leave the server.</value>
<data name="mcc.password_hidden" xml:space="preserve">
<value>Password(invisible): </value>
</data>
<data name="mcc.yggdrasil_host" xml:space="preserve">
<value>Authlib server host:</value>
</data>
<data name="mcc.yggdrasil_port" xml:space="preserve">
<value>Authlib server port [{0}]:</value>
</data>
<data name="mcc.yggdrasil_api_path" xml:space="preserve">
<value>Authlib-Injector API path [{0}]:</value>
</data>
<data name="mcc.yggdrasil_use_https" xml:space="preserve">
<value>Use HTTPS? [Y/n]</value>
</data>
<data name="mcc.yggdrasil_invalid_host" xml:space="preserve">
<value>The authlib server host cannot be empty.</value>
</data>
<data name="mcc.yggdrasil_invalid_port" xml:space="preserve">
<value>The authlib server port must be between 1 and 65535.</value>
</data>
<data name="mcc.yggdrasil_invalid_yes_no" xml:space="preserve">
<value>Please answer yes or no.</value>
</data>
<data name="mcc.player_dead" xml:space="preserve">
<value>You are dead. Type '{0}respawn' to respawn.</value>
</data>

View file

@ -209,6 +209,7 @@ namespace MinecraftClient
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
string tomlString = TomletMain.TomlStringFrom(Config);
Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
tomlString = RemoveLegacyAuthServerSection(tomlString);
string[] tomlList = tomlString.Split('\n');
StringBuilder newConfig = new();
@ -272,6 +273,19 @@ namespace MinecraftClient
}
}
private static string RemoveLegacyAuthServerSection(string tomlString)
{
const string legacySection = "[Main.General.AuthServer]";
int sectionStart = tomlString.IndexOf(legacySection, StringComparison.Ordinal);
if (sectionStart < 0)
return tomlString;
int nextSection = tomlString.IndexOf("\n[", sectionStart + legacySection.Length, StringComparison.Ordinal);
return nextSection < 0
? tomlString[..sectionStart]
: tomlString.Remove(sectionStart, nextSection - sectionStart + 1);
}
/// <summary>
/// Load settings from the command line
/// </summary>
@ -656,6 +670,8 @@ namespace MinecraftClient
General.Account.Login ??= string.Empty;
General.Account.Password ??= string.Empty;
if (!General.MigrateLegacyAuthServer())
ConsoleIO.WriteLogLine(Translations.config_auth_server_url_invalid);
if (!InternalConfig.KeepAccountSettings)
{
if (Advanced.AccountList.TryGetValue(General.Account.Login, out AccountInfoConfig account))
@ -753,12 +769,72 @@ namespace MinecraftClient
[TomlInlineComment("$Main.General.method$")]
public LoginMethod Method = LoginMethod.mcc;
[TomlInlineComment("$Main.General.AuthServerUrl$")]
public string AuthServerUrl = string.Empty;
// Retained only to deserialize pre-URL configs. WriteToFile() removes this legacy section.
[TomlInlineComment("$Main.General.AuthlibServer$")]
public AuthlibServer AuthServer = new();
[TomlInlineComment("$Main.General.AuthlibUser$")]
public string AuthUser = "";
public bool MigrateLegacyAuthServer()
{
AuthServerUrl ??= string.Empty;
if (TrySetAuthServerUrl(AuthServerUrl))
return true;
if (!string.IsNullOrWhiteSpace(AuthServerUrl))
return false;
if (!string.IsNullOrWhiteSpace(AuthServer.Host))
{
string path = AuthServer.AuthlibInjectorAPIPath ?? string.Empty;
if (!path.StartsWith('/'))
path = '/' + path;
string legacyUrl = $"{(AuthServer.UseHttps ? "https" : "http")}://{AuthServer.Host}:{AuthServer.Port}{path}";
return TrySetAuthServerUrl(legacyUrl);
}
return true;
}
public bool TrySetAuthServerUrl(string url)
{
if (!TryGetNormalizedAuthServerUri(url, out Uri? authServerUri))
return false;
AuthServerUrl = authServerUri.AbsoluteUri;
return true;
}
public bool TryGetAuthServerUri([NotNullWhen(true)] out Uri? authServerUri)
=> TryGetNormalizedAuthServerUri(AuthServerUrl, out authServerUri);
private static bool TryGetNormalizedAuthServerUri(string? url, [NotNullWhen(true)] out Uri? authServerUri)
{
authServerUri = null;
if (!Uri.TryCreate(url?.Trim(), UriKind.Absolute, out Uri? parsedUri)
|| (!parsedUri.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
&& !parsedUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|| string.IsNullOrWhiteSpace(parsedUri.Host)
|| !string.IsNullOrEmpty(parsedUri.UserInfo)
|| !string.IsNullOrEmpty(parsedUri.Query)
|| !string.IsNullOrEmpty(parsedUri.Fragment))
{
return false;
}
var builder = new UriBuilder(parsedUri)
{
Path = parsedUri.AbsolutePath.TrimEnd('/') + "/"
};
authServerUri = builder.Uri;
return true;
}
public enum LoginType { mojang, microsoft, yggdrasil };

View file

@ -138,6 +138,16 @@ Coordinate = { x = 145, y = 64, z = 2045 }
AccountType = "microsoft"
```
#### Interactive login
If both `Account.Login` and `Account.Password` are empty when MCC starts, it asks which account type to use instead of assuming Microsoft login.
- Choose Offline to enter an in-game username. MCC saves the account as an offline account.
- Choose Online (Microsoft) to continue with the device-code sign-in. MCC shows the code and link, then opens the sign-in page in your browser.
- Choose Yggdrasil to enter the username, password, and authlib-injector URL for the account server. MCC checks the URL before it continues. If the address is malformed, unreachable, or does not return authlib-injector metadata, MCC explains the problem and asks for another URL.
After a successful login, MCC saves the account details and selected account type in the configuration file. Later starts use those saved details and do not show this prompt. To choose a different method, edit or clear the account settings in the configuration file.
#### `Method`
- **Description:**
@ -154,52 +164,32 @@ Coordinate = { x = 145, y = 64, z = 2045 }
Method = "mcc"
```
#### `AuthServer`
#### `AuthServerUrl`
- **Description:**
This subsection is used when `AccountType` is set to `yggdrasil`. It points MCC at the authlib/Yggdrasil server used for login, session checks, and profile key requests.
Use this setting with `AccountType = "yggdrasil"`. It is the complete base URL of the authlib-injector or Yggdrasil service MCC uses for login, session checks, and profile key requests.
MCC now writes this as a dedicated TOML subsection instead of an inline table:
The URL must start with `http://` or `https://` and include any path used by the service. For example, an authlib-injector server may use `/authlib-injector/` as its base path.
```toml
[Main.General.AuthServer]
```
An existing multi-field Yggdrasil configuration is converted to this URL automatically. When MCC writes the configuration after migration, it removes the previous Yggdrasil section.
`Host` accepts either a plain host name or a `host:port` pair. If you include the port there, MCC updates `Port` to match.
- **Type:** `string`
`AuthlibInjectorAPIPath` defaults to `/api/yggdrasil`. Change it if your authlib-injector server uses a different prefix, such as `/authlib-injector`.
`UseHttps` defaults to `true`. Set it to `false` if your local or development auth server only exposes plain HTTP.
- **Type:** `section`
- **Default:**
```toml
[Main.General.AuthServer]
Port = 443
AuthlibInjectorAPIPath = "/api/yggdrasil"
UseHttps = true
Host = ""
```
- **Default:** `""`
- **Example:**
```
[Main.General.AuthServer]
Host = "auth.example.com"
Port = 443
AuthlibInjectorAPIPath = "/api/yggdrasil"
UseHttps = true
```toml
Account = { Login = "player@example.com", Password = "password" }
AccountType = "yggdrasil"
AuthServerUrl = "https://auth.example.com/api/yggdrasil/"
```
```
[Main.General.AuthServer]
Host = "127.0.0.1"
Port = 25585
AuthlibInjectorAPIPath = "/authlib-injector"
UseHttps = false
```toml
Account = { Login = "player", Password = "password" }
AccountType = "yggdrasil"
AuthServerUrl = "http://127.0.0.1:25585/authlib-injector/"
```
#### `AuthUser`