diff --git a/NeosModLoader/ModLoaderConfiguration.cs b/NeosModLoader/ModLoaderConfiguration.cs index 4c96ca4..8dfe17b 100644 --- a/NeosModLoader/ModLoaderConfiguration.cs +++ b/NeosModLoader/ModLoaderConfiguration.cs @@ -1,74 +1,120 @@ +using FrooxEngine; +using HarmonyLib; +using NeosModLoader.Utility; using System; +using System.Collections.Generic; +using System.ComponentModel; using System.IO; +using System.Linq; using System.Reflection; namespace NeosModLoader { internal class ModLoaderConfiguration { + private static readonly Lazy _configuration = new(LoadConfig); private static readonly string CONFIG_FILENAME = "NeosModLoader.config"; - private static ModLoaderConfiguration? _configuration; + public bool AdvertiseVersion { get; private set; } = false; + + public bool Debug { get; private set; } = false; - internal static ModLoaderConfiguration Get() + public bool ExposeLateTypes { - if (_configuration == null) - { - // the config file can just sit next to the dll. Simple. - string path = Path.Combine(GetAssemblyDirectory(), CONFIG_FILENAME); - _configuration = new ModLoaderConfiguration(); + get => !HideLateTypes; + set => HideLateTypes = !value; + } + + public bool ExposeModTypes + { + get => !HideModTypes; + set => HideModTypes = !value; + } + + public bool HideConflicts + { + get => !LogConflicts; + set => LogConflicts = !value; + } + + public bool HideLateTypes { get; private set; } = true; + + public bool HideModTypes { get; private set; } = true; + + public bool HideVisuals { get; private set; } = false; + + public bool LogConflicts { get; private set; } = true; + + public bool NoLibraries { get; private set; } = false; + + public bool NoMods { get; private set; } = false; + + public bool Unsafe { get; private set; } = false; + + internal static ModLoaderConfiguration Get() => _configuration.Value; + + private static string GetAssemblyDirectory() + { + var codeBase = Assembly.GetExecutingAssembly().CodeBase; + var uri = new UriBuilder(codeBase); + var path = Uri.UnescapeDataString(uri.Path); + + return Path.GetDirectoryName(path); + } + + private static string? GetConfigPath() + { + if (LaunchArguments.TryGetArgument("Config.Path", out var argument)) + return argument.Value; + + // the config file can just sit next to the dll. Simple. + return Path.Combine(GetAssemblyDirectory(), CONFIG_FILENAME); + } + private static ModLoaderConfiguration LoadConfig() + { + var path = GetConfigPath(); + var config = new ModLoaderConfiguration(); + + var configOptions = typeof(ModLoaderConfiguration).GetProperties(AccessTools.all).ToArray(); + + if (!File.Exists(path)) + { + Logger.MsgInternal($"Using default config - file doesn't exist: {path}"); + } + else + { // .NET's ConfigurationManager is some hot trash to the point where I'm just done with it. // Time to reinvent the wheel. This parses simple key=value style properties from a text file. try { + var unknownKeys = new List(); var lines = File.ReadAllLines(path); + foreach (var line in lines) { - int splitIdx = line.IndexOf('='); - if (splitIdx != -1) + var splitIdx = line.IndexOf('='); + if (splitIdx == -1) + continue; + + string key = line.Substring(0, splitIdx).Trim(); + string value = line.Substring(splitIdx + 1).Trim(); + + var possibleProperty = configOptions.FirstOrDefault(property => property.Name.Equals(key, StringComparison.InvariantCultureIgnoreCase)); + if (possibleProperty == null) { - string key = line.Substring(0, splitIdx); - string value = line.Substring(splitIdx + 1); - - if ("unsafe".Equals(key) && "true".Equals(value)) - { - _configuration.Unsafe = true; - } - else if ("debug".Equals(key) && "true".Equals(value)) - { - _configuration.Debug = true; - } - else if ("hidevisuals".Equals(key) && "true".Equals(value)) - { - _configuration.HideVisuals = true; - } - else if ("nomods".Equals(key) && "true".Equals(value)) - { - _configuration.NoMods = true; - } - else if ("nolibraries".Equals(key) && "true".Equals(value)) - { - _configuration.NoLibraries = true; - } - else if ("advertiseversion".Equals(key) && "true".Equals(value)) - { - _configuration.AdvertiseVersion = true; - } - else if ("logconflicts".Equals(key) && "false".Equals(value)) - { - _configuration.LogConflicts = false; - } - else if ("hidemodtypes".Equals(key) && "false".Equals(value)) - { - _configuration.HideModTypes = false; - } - else if ("hidelatetypes".Equals(key) && "false".Equals(value)) - { - _configuration.HideLateTypes = false; - } + unknownKeys.Add(key); + continue; } + + var parsedValue = TypeDescriptor.GetConverter(possibleProperty.PropertyType).ConvertFromInvariantString(value); + possibleProperty.SetValue(config, parsedValue); + + Logger.MsgInternal($"Loaded value for {possibleProperty.Name} from file: {parsedValue}"); } + + Logger.WarnInternal($"Unknown key found in config file: {string.Join(", ", unknownKeys)}"); + Logger.WarnInternal($"Supported keys: {string.Join(", ", configOptions.Select(property => $"{property.PropertyType} {property.Name}"))}"); } catch (Exception e) { @@ -86,25 +132,35 @@ internal static ModLoaderConfiguration Get() } } } - return _configuration; + + var boolType = typeof(bool); + foreach (var option in configOptions) + { + if (LaunchArguments.TryGetArgument($"Config.{option.Name}", out var argument)) + { + if (option.PropertyType == boolType) + { + option.SetValue(config, true); + Logger.MsgInternal($"Enabling [{option.Name}] from launch flag"); + + if (!argument.IsFlag) + Logger.WarnInternal("Found possible misplaced parameter value after this flag argument"); + } + else if (!argument.IsFlag) + { + config.SetProperty(option, argument.Value!); + Logger.MsgInternal($"Setting [{option.Name}] from launch flag: {argument.Value}"); + } + } + } + + return config; } - private static string GetAssemblyDirectory() + private void SetProperty(PropertyInfo property, string value) { - string codeBase = Assembly.GetExecutingAssembly().CodeBase; - UriBuilder uri = new(codeBase); - string path = Uri.UnescapeDataString(uri.Path); - return Path.GetDirectoryName(path); + var parsedValue = TypeDescriptor.GetConverter(property.PropertyType).ConvertFromInvariantString(value); + property.SetValue(this, parsedValue); } - - public bool Unsafe { get; private set; } = false; - public bool Debug { get; private set; } = false; - public bool HideVisuals { get; private set; } = false; - public bool NoMods { get; private set; } = false; - public bool NoLibraries { get; private set; } = false; - public bool AdvertiseVersion { get; private set; } = false; - public bool LogConflicts { get; private set; } = true; - public bool HideModTypes { get; private set; } = true; - public bool HideLateTypes { get; private set; } = true; } } diff --git a/NeosModLoader/Utility/LaunchArguments.cs b/NeosModLoader/Utility/LaunchArguments.cs new file mode 100644 index 0000000..a6462e9 --- /dev/null +++ b/NeosModLoader/Utility/LaunchArguments.cs @@ -0,0 +1,354 @@ +using FrooxEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static FrooxEngine.FinalIK.IKSolverVR; + +namespace NeosModLoader.Utility +{ + /// + /// Contains methods to access the command line arguments Neos was launched with. + /// + /// Use analyze on Environment.GetCommandLineArgs() to find all possible arguments. + public static class LaunchArguments + { + /// + /// Prefix symbol that indicates an argument name: - + /// + public const char ArgumentIndicator = '-'; + + /// + /// Prefix string after the argument indicator, that indicated an argument targeted at NML itself or mods loaded by it: NML.
+ /// This is removed from argument names to get their proper name. + ///
+ public const string NMLArgumentPrefix = "NML."; + + private static readonly Dictionary arguments = new(StringComparer.InvariantCultureIgnoreCase); + + private static readonly string[] possibleNeosExactFlagArguments = + { + "ctaa", "ctaatemporaledgepower", "ctaasharpnessenabled", "ctaaadaptivesharpness", "ForceSRAnipal", + "StereoDisplay", "MixedReality", "DirectComposition", "ExternalComposition", "create_mrc_config", + "load_mrc_config" + }; + + private static readonly string[] possibleNeosExactParameterArguments = + { + "TextureSizeRatio", "DataPath", "CachePath" + }; + + private static readonly string[] possibleNeosInfixFlagArguments = + { + "CameraBiggestGroup", "CameraTimelapse", "CameraStayBehind", "CameraStayInFront", "AnnounceHomeOnLAN" + }; + + private static readonly string[] possibleNeosSuffixFlagArguments = + { + "GeneratePrecache", "Verbose", "pro", "UseLocalCloud", "UseStagingCloud", "ForceRelay", "Invisible", + "ForceReticleAboveHorizon", "ForceNoVoice", "ResetDash", "Kiosk", "DontAutoOpenCloudHome", "ResetUserspace", + "ForceLANOnly", "DeleteUnsyncedCloudRecords", "ForceSyncConflictingCloudRecords", "ForceIntroTutorial", + "SkipIntroTutorial", "RepairDatabase", "UseNeosCamera", "LegacySteamVRInput", "Etee", "HideScreenReticle", + "Viveport", "DisableNativeTextureUpload" + }; + + private static readonly string[] possibleNeosSuffixParameterArguments = + { + "Config", "LoadAssembly", "BackgroundWorkers", "PriorityWorkers", "Bench", "Watchdog", "Bootstrap", + "OnlyHost", "Join", "Open", "OpenUnsafe", "EnableOWO" + }; + + /// + /// Gets all arguments Neos was launched with. + /// + public static IEnumerable Arguments + { + get + { + foreach (var argument in arguments.Values) + yield return argument; + } + } + + /// + /// Gets the path to the launched Neos executable. + /// + public static string ExecutablePath { get; } + + /// + /// Gets the names of all arguments recognized by Neos.
+ /// These have different rules relating to the names of other arguments. + ///
+ public static IEnumerable PossibleNeosArguments + => PossibleNeosParameterArguments.Concat(PossibleNeosFlagArguments); + + /// + /// Gets the names of all exact flag arguments recognized by Neos.
+ /// These are forbidden as the exact names of any other arguments. + ///
+ public static IEnumerable PossibleNeosExactFlagArguments + { + get + { + foreach (var argument in possibleNeosExactFlagArguments) + yield return argument; + } + } + + /// + /// Gets the names of all exact parameter arguments recognized by Neos.
+ /// These are forbidden as the exact names of any other arguments. + ///
+ public static IEnumerable PossibleNeosExactParameterArguments + { + get + { + foreach (var argument in possibleNeosExactParameterArguments) + yield return argument; + } + } + + /// + /// Gets the names of all flag arguments recognized by Neos.
+ /// These have different rules relating to the names of other arguments. + ///
+ public static IEnumerable PossibleNeosFlagArguments + => possibleNeosExactFlagArguments.Concat(possibleNeosSuffixFlagArguments).Concat(possibleNeosInfixFlagArguments); + + /// + /// Gets the names of all infix flag arguments recognized by Neos.
+ /// These are forbidden as infixes of any other arguments. + ///
+ public static IEnumerable PossibleNeosInfixFlagArguments + { + get + { + foreach (var argument in possibleNeosInfixFlagArguments) + yield return argument; + } + } + + /// + /// Gets the names of all parameter arguments recognized by Neos.
+ /// These have different rules relating to the names of other arguments. + ///
+ public static IEnumerable PossibleNeosParameterArguments + => possibleNeosExactParameterArguments.Concat(possibleNeosSuffixParameterArguments); + + /// + /// Gets the names of all suffix flag arguments recognized by Neos.
+ /// These are forbidden as suffixes of any other arguments. + ///
+ public static IEnumerable PossibleNeosSuffixFlagArguments + { + get + { + foreach (var flagArgument in possibleNeosSuffixFlagArguments) + yield return flagArgument; + } + } + + /// + /// Gets the names of all suffix parameter arguments recognized by Neos.
+ /// These are forbidden as suffixes of any other arguments. + ///
+ public static IEnumerable PossibleNeosSuffixParameterArguments + { + get + { + foreach (var argument in possibleNeosSuffixParameterArguments) + yield return argument; + } + } + + static LaunchArguments() + { + possibleNeosExactFlagArguments = possibleNeosExactFlagArguments + .Concat( + Enum.GetValues(typeof(HeadOutputDevice)) + .Cast() + .Select(v => v.ToString())) + .ToArray(); + + // First argument is the path of the executable + var args = Environment.GetCommandLineArgs(); + ExecutablePath = args[0]; + + var i = 1; + while (i < args.Length) + { + var arg = args[i++].TrimStart(ArgumentIndicator); + var hasParameter = i < args.Length && args[i].FirstOrDefault() != ArgumentIndicator && MatchNeosArgument(args[i]) == null; + + var matchedNeosArgument = MatchNeosFlagArgument(arg); + if (matchedNeosArgument != null) + { + arguments.Add(matchedNeosArgument, new Argument(Target.Neos, i, arg, matchedNeosArgument)); + + if (hasParameter) + Logger.WarnInternal($"Possible misplaced parameter value after flag argument: {matchedNeosArgument}"); + + continue; + } + + matchedNeosArgument = MatchNeosParameterArgument(arg); + if (matchedNeosArgument != null) + { + if (hasParameter) + arguments.Add(matchedNeosArgument, new Argument(Target.Neos, i, arg, matchedNeosArgument, args[i++])); + else + Logger.WarnInternal($"Expected parameter for argument: {matchedNeosArgument}"); + + continue; + } + + if (!arg.StartsWith(NMLArgumentPrefix, StringComparison.InvariantCultureIgnoreCase)) + { + // The value of an unknown argument is not skipped, but added as its own argument in the next iteration as well + arguments.Add(arg, new Argument(Target.Unknown, i, arg, arg, hasParameter ? args[i] : null)); + continue; + } + + var name = arg.Substring(NMLArgumentPrefix.Length); + arguments.Add(name, new Argument(Target.NML, i, arg, name, hasParameter ? args[i++] : null)); + } + + foreach (var argument in arguments) + Logger.MsgInternal($"Parsed {argument.Value}"); + } + + /// + /// Gets the with the given proper name. + /// + /// The proper name of the argument. + /// The with the given proper name. + /// + /// + public static Argument GetArgument(string name) + => arguments[name]; + + /// + /// Checks whether an argument with the given proper name is present. + /// + /// The proper name of the argument. + /// true if such an argument exists, otherwise false. + public static bool IsPresent(string name) + => arguments.ContainsKey(name); + + /// + /// Tries to find one of the that matches the given name in the right way. + /// + /// The name to check. + /// The matched Neos launch argument or null if there's no match. + public static string? MatchNeosArgument(string name) + => MatchNeosParameterArgument(name) + ?? MatchNeosFlagArgument(name); + + /// + /// Tries to find one of the that matches the given name in the right way. + /// + /// The name to check. + /// The matched Neos launch argument flags or null if there's no match. + public static string? MatchNeosFlagArgument(string name) + => possibleNeosExactFlagArguments.FirstOrDefault(neosArg => name.Equals(neosArg, StringComparison.InvariantCultureIgnoreCase)) + ?? possibleNeosSuffixFlagArguments.FirstOrDefault(neosArg => name.EndsWith(neosArg, StringComparison.InvariantCultureIgnoreCase)) + ?? possibleNeosInfixFlagArguments.FirstOrDefault(neosArg => name.IndexOf(neosArg, StringComparison.InvariantCultureIgnoreCase) >= 0); + + /// + /// Tries to find one of the that matches the given name in the right way. + /// + /// The name to check. + /// The matched Neos launch argument or null if there's no match. + public static string? MatchNeosParameterArgument(string name) + => possibleNeosExactParameterArguments.FirstOrDefault(neosArg => name.Equals(neosArg, StringComparison.InvariantCultureIgnoreCase)) + ?? possibleNeosSuffixParameterArguments.FirstOrDefault(neosArg => name.EndsWith(neosArg, StringComparison.InvariantCultureIgnoreCase)); + + /// + /// Tries to get the with the given proper name. + /// + /// The proper name of the argument. + /// The with the given proper name or default() if it's not present. + /// true if such an argument exists, otherwise false. + /// + public static bool TryGetArgument(string name, out Argument argument) + => arguments.TryGetValue(name, out argument); + + /// + /// Data structure for launch arguments. + /// + public readonly struct Argument + { + /// + /// Gets the index of this argument in the array returned by . + /// + public int Index { get; } + + /// + /// Gets whether the argument is a flag, i.e. whether it doesn't have a value. + /// + public bool IsFlag => Value == null; + + /// + /// Gets the proper name of the argument.
+ /// For this is the name Neos looks for, + /// while for this is the name without the NML Argument Prefix. + ///
+ public string Name { get; } + + /// + /// Gets the raw name of the argument, as it is on the command line. + /// + public string RawName { get; } + + /// + /// Gets the target that the argument is for. + /// + public Target Target { get; } + + /// + /// Gets the value associated with the argument. Is null for flags. + /// + public string? Value { get; } + + internal Argument(Target target, int index, string rawName, string name, string? value = null) + { + Target = target; + Index = index - 1; + RawName = rawName; + Name = name; + Value = value; + } + + /// + /// Gets a string representation of this parsed argument. + /// + /// A string representing this parsed argument. + public override string ToString() + { + return $"{Target} Argument {(Name.Equals(RawName, StringComparison.InvariantCultureIgnoreCase) ? Name : $"{RawName} ({Name})")}: {(IsFlag ? "present" : $"\"{Value}\"")}"; + } + } + + /// + /// Different possible targets for command line arguments. + /// + public enum Target + { + /// + /// Not a known target. + /// + Unknown, + + /// + /// Targeted at Neos itself. + /// + Neos, + + /// + /// Targeted at NML or a mod it loads. + /// + NML + } + } +}