using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using MinecraftClient.Protocol;
using MinecraftClient.Proxy;
using Tomlet;
using Tomlet.Attributes;
using Tomlet.Models;
using static MinecraftClient.Settings.AppVarConfigHelper;
using static MinecraftClient.Settings.ChatBotConfigHealper;
using static MinecraftClient.Settings.ChatFormatConfigHelper;
using static MinecraftClient.Settings.ConsoleConfigHealper;
using static MinecraftClient.Settings.HeadCommentHealper;
using static MinecraftClient.Settings.LoggingConfigHealper;
using static MinecraftClient.Settings.MainConfigHelper;
using static MinecraftClient.Settings.MainConfigHelper.MainConfig;
using static MinecraftClient.Settings.MainConfigHelper.MainConfig.AdvancedConfig;
using static MinecraftClient.Settings.MCSettingsConfigHealper;
using static MinecraftClient.Settings.SignatureConfigHelper;
namespace MinecraftClient
{
public static class Settings
{
private const int CommentsAlignPosition = 45;
private readonly static Regex CommentRegex = new(@"^(.*)\s?#\s\$([\w\.]+)\$\s*$$", RegexOptions.Compiled);
// Other Settings
public const string TranslationsFile_Version = "1.19.3";
public const string TranslationsFile_Website_Index = "https://piston-meta.mojang.com/v1/packages/c492375ded5da34b646b8c5c0842a0028bc69cec/2.json";
public const string TranslationsFile_Website_Download = "https://resources.download.minecraft.net";
public const string TranslationProjectUrl = "https://crowdin.com/project/minecraft-console-client";
public const int ClientTicksPerSecond = 20;
public const int ClientTickIntervalMilliseconds = 1000 / ClientTicksPerSecond;
public static GlobalConfig Config = new();
public static class InternalConfig
{
public static string ServerIP = String.Empty;
public static ushort ServerPort = 25565;
public static AccountInfoConfig Account = new();
public static string Username = string.Empty;
public static string MinecraftVersion = string.Empty;
public static bool InteractiveMode = true;
public static bool GravityEnabled = true;
public static bool KeepAccountSettings = false;
public static bool KeepServerSettings = false;
}
public class GlobalConfig
{
[TomlPrecedingComment("$Head$")]
public HeadComment Head
{
get { return HeadCommentHealper.Config; }
set { HeadCommentHealper.Config = value; HeadCommentHealper.Config.OnSettingUpdate(); }
}
public MainConfig Main
{
get { return MainConfigHelper.Config; }
set { MainConfigHelper.Config = value; MainConfigHelper.Config.OnSettingUpdate(); }
}
[TomlPrecedingComment("$Signature$")]
public SignatureConfig Signature
{
get { return SignatureConfigHelper.Config; }
set { SignatureConfigHelper.Config = value; SignatureConfigHelper.Config.OnSettingUpdate(); }
}
[TomlPrecedingComment("$Logging$")]
public LoggingConfig Logging
{
get { return LoggingConfigHealper.Config; }
set { LoggingConfigHealper.Config = value; LoggingConfigHealper.Config.OnSettingUpdate(); }
}
public ConsoleConfig Console
{
get { return ConsoleConfigHealper.Config; }
set { ConsoleConfigHealper.Config = value; ConsoleConfigHealper.Config.OnSettingUpdate(); }
}
public AppVarConfig AppVar
{
get { return AppVarConfigHelper.Config; }
set { AppVarConfigHelper.Config = value; AppVarConfigHelper.Config.OnSettingUpdate(); }
}
[TomlPrecedingComment("$Proxy$")]
public ProxyHandler.Configs Proxy
{
get { return ProxyHandler.Config; }
set { ProxyHandler.Config = value; ProxyHandler.Config.OnSettingUpdate(); }
}
[TomlPrecedingComment("$MCSettings$")]
public MCSettingsConfig MCSettings
{
get { return MCSettingsConfigHealper.Config; }
set { MCSettingsConfigHealper.Config = value; MCSettingsConfigHealper.Config.OnSettingUpdate(); }
}
[TomlPrecedingComment("$ChatFormat$")]
public ChatFormatConfig ChatFormat
{
get { return ChatFormatConfigHelper.Config; }
set { ChatFormatConfigHelper.Config = value; ChatFormatConfigHelper.Config.OnSettingUpdate(); }
}
[TomlPrecedingComment("$ChatBot$")]
public ChatBotConfig ChatBot
{
get { return ChatBotConfigHealper.Config; }
set { ChatBotConfigHealper.Config = value; }
}
}
///
/// Structured result returned by .
///
public readonly struct ConfigLoadResult
{
public bool Success { get; init; }
public bool NeedWriteDefault { get; init; }
/// True when a pre-TOML legacy config was detected, backed up, and a fresh default is needed.
public bool IsLegacyUpgrade { get; init; }
/// Non-null when the load failed due to a parse/IO error (not a legacy upgrade).
public string? ErrorMessage { get; init; }
/// Path where the old config was backed up (legacy upgrade case).
public string? LegacyBackupPath { get; init; }
}
public static ConfigLoadResult LoadFromFile(string filepath, bool keepAccountAndServerSettings = false)
{
bool keepAccountSettings = InternalConfig.KeepAccountSettings;
bool keepServerSettings = InternalConfig.KeepServerSettings;
if (keepAccountAndServerSettings)
InternalConfig.KeepAccountSettings = InternalConfig.KeepServerSettings = true;
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
TomlDocument document;
try
{
document = TomlParser.ParseFile(filepath);
Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
Config = TomletMain.To(document);
}
catch (Exception ex)
{
Thread.CurrentThread.CurrentCulture = Program.ActualCulture;
try
{
string configString = File.ReadAllText(filepath);
if (configString.Contains("Some settings missing here after an upgrade?"))
{
string newFilePath = Path.ChangeExtension(filepath, ".old.ini");
File.Copy(filepath, newFilePath, true);
return new ConfigLoadResult
{
Success = false,
NeedWriteDefault = true,
IsLegacyUpgrade = true,
LegacyBackupPath = newFilePath
};
}
}
catch { }
return new ConfigLoadResult
{
Success = false,
NeedWriteDefault = false,
ErrorMessage = ex.GetFullMessage()
};
}
finally
{
if (!keepAccountSettings)
InternalConfig.KeepAccountSettings = false;
if (!keepServerSettings)
InternalConfig.KeepServerSettings = false;
}
return new ConfigLoadResult { Success = true, NeedWriteDefault = false };
}
public static void WriteToFile(string filepath, bool backupOldFile)
{
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();
foreach (string line in tomlList)
{
Match matchComment = CommentRegex.Match(line);
if (matchComment.Success && matchComment.Groups.Count == 3)
{
string config = matchComment.Groups[1].Value, comment = matchComment.Groups[2].Value;
if (config.Length > 0)
newConfig.Append(config).Append(' ', Math.Max(1, CommentsAlignPosition - config.Length) - 1);
string? comment_trans = ConfigComments.ResourceManager.GetString(comment);
if (string.IsNullOrEmpty(comment_trans))
newConfig.Append("# ").AppendLine(comment.ReplaceLineEndings());
else
newConfig.Append("# ").AppendLine(comment_trans.Replace("\n", "\n# ").ReplaceLineEndings());
}
else
{
newConfig.AppendLine(line);
}
}
bool needUpdate = true;
byte[] newConfigByte = Encoding.UTF8.GetBytes(newConfig.ToString());
if (File.Exists(filepath))
{
try
{
if (new FileInfo(filepath).Length == newConfigByte.Length)
if (File.ReadAllBytes(filepath).SequenceEqual(newConfigByte))
needUpdate = false;
}
catch { }
}
if (needUpdate)
{
bool backupSuccessed = true;
if (backupOldFile && File.Exists(filepath))
{
string backupFilePath = Path.ChangeExtension(filepath, ".backup.ini");
try { File.Copy(filepath, backupFilePath, true); }
catch (Exception ex)
{
backupSuccessed = false;
ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_backup_fail, backupFilePath));
ConsoleIO.WriteLine(ex.Message);
}
}
if (backupSuccessed)
{
try { File.WriteAllBytes(filepath, newConfigByte); }
catch (Exception ex)
{
ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_write_fail, filepath));
ConsoleIO.WriteLine(ex.Message);
}
}
}
}
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);
}
///
/// Load settings from the command line
///
/// Command-line arguments
/// Thrown on invalid arguments
public static void LoadArguments(string[] args)
{
int positionalIndex = 0;
bool skipPassword = false;
foreach (string argument in args)
{
if (argument.StartsWith("--"))
{
//Load settings as --setting=value and --section.setting=value
if (!argument.Contains('='))
throw new ArgumentException(string.Format(Translations.error_setting_argument_syntax, argument));
string argumentBody = argument[2..];
int separatorIndex = argumentBody.IndexOf('=');
if (separatorIndex < 1)
throw new ArgumentException(string.Format(Translations.error_setting_argument_syntax, argument));
string settingName = argumentBody[..separatorIndex].Trim();
string settingValue = argumentBody[(separatorIndex + 1)..].Trim();
if (!TryApplyArgumentSetting(settingName, settingValue))
throw new ArgumentException(string.Format(Translations.error_setting_argument_syntax, argument));
}
else if (argument.StartsWith("-") && argument.Length > 1)
{
//Keep single dash arguments as unsupported for now (future use)
throw new ArgumentException(string.Format(Translations.error_setting_argument_syntax, argument));
}
else
{
switch (positionalIndex)
{
case 0:
if (Config.Main.Advanced.AccountList.TryGetValue(argument, out AccountInfoConfig accountInfo))
{
InternalConfig.Account = accountInfo;
skipPassword = true;
}
else
{
InternalConfig.Account.Login = argument;
}
InternalConfig.KeepAccountSettings = true;
break;
case 1:
if (!skipPassword)
InternalConfig.Account.Password = argument;
break;
case 2:
Config.Main.SetServerIP(new MainConfig.ServerInfoConfig(argument), true);
InternalConfig.KeepServerSettings = true;
break;
case 3:
// SingleCommand = argument;
break;
}
positionalIndex++;
}
}
}
private static readonly string[][] s_argumentSettingSearchPrefixes =
[
[],
["Main", "Advanced"],
["Main", "General"],
["Main"],
["Logging"],
["Console", "General"],
["Console", "CommandSuggestion"],
["Console"],
["MCSettings"],
["ChatFormat"],
["ChatBot"],
["Signature"],
["Proxy"],
["AppVar"]
];
private static bool TryApplyArgumentSetting(string settingPath, string settingValue)
{
if (string.IsNullOrWhiteSpace(settingPath))
return false;
string[] settingParts = settingPath
.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (settingParts.Length == 0)
return false;
HashSet triedPaths = new(StringComparer.Ordinal);
foreach (string[] prefix in s_argumentSettingSearchPrefixes)
{
string[] fullPath = [.. prefix, .. settingParts];
string pathKey = string.Join('.', fullPath.Select(NormalizeArgumentToken));
if (!triedPaths.Add(pathKey))
continue;
if (!TryResolveArgumentSettingPath(fullPath, out object owner, out MemberInfo member, out List