diff --git a/.editorconfig b/.editorconfig index 243b28e..cd4e246 100644 --- a/.editorconfig +++ b/.editorconfig @@ -60,34 +60,33 @@ dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion diff --git a/.run/Attach to Resonite.run.xml b/.run/Attach to Resonite.run.xml new file mode 100644 index 0000000..7b51fad --- /dev/null +++ b/.run/Attach to Resonite.run.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/ResoniteModLoader/AssemblyHider.cs b/ResoniteModLoader/AssemblyHider.cs index 6f4b33e..68352dd 100644 --- a/ResoniteModLoader/AssemblyHider.cs +++ b/ResoniteModLoader/AssemblyHider.cs @@ -48,27 +48,25 @@ internal static class AssemblyHider { /// Our RML harmony instance /// Assemblies that were loaded when RML first started internal static void PatchResonite(Harmony harmony, HashSet initialAssemblies) { - //if (ModLoaderConfiguration.Get().HideModTypes) { - // initialize the static assembly sets that our patches will need later - resoniteAssemblies = GetResoniteAssemblies(initialAssemblies); - modAssemblies = GetModAssemblies(resoniteAssemblies); - dotNetAssemblies = resoniteAssemblies.Where(LooksLikeDotNetAssembly).ToHashSet(); - - // TypeHelper.FindType explicitly does a type search - MethodInfo findTypeTarget = AccessTools.DeclaredMethod(typeof(TypeHelper), nameof(TypeHelper.FindType), new Type[] { typeof(string) }); - MethodInfo findTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(FindTypePostfix)); - harmony.Patch(findTypeTarget, postfix: new HarmonyMethod(findTypePatch)); - - // ReflectionExtensions.IsValidGenericType checks a type for validity, and if it returns `true` it reveals that the type exists - MethodInfo isValidGenericTypeTarget = AccessTools.DeclaredMethod(typeof(ReflectionExtensions), nameof(ReflectionExtensions.IsValidGenericType), new Type[] { typeof(Type), typeof(bool) }); - MethodInfo isValidGenericTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(IsValidTypePostfix)); - harmony.Patch(isValidGenericTypeTarget, postfix: new HarmonyMethod(isValidGenericTypePatch)); - - // FrooxEngine likes to enumerate all types in all assemblies, which is prone to issues (such as crashing FrooxCode if a type isn't loadable) - MethodInfo getAssembliesTarget = AccessTools.DeclaredMethod(typeof(AppDomain), nameof(AppDomain.GetAssemblies), Array.Empty()); - MethodInfo getAssembliesPatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(GetAssembliesPostfix)); - harmony.Patch(getAssembliesTarget, postfix: new HarmonyMethod(getAssembliesPatch)); - //} + // initialize the static assembly sets that our patches will need later + resoniteAssemblies = GetResoniteAssemblies(initialAssemblies); + modAssemblies = GetModAssemblies(resoniteAssemblies); + dotNetAssemblies = resoniteAssemblies.Where(LooksLikeDotNetAssembly).ToHashSet(); + + // TypeHelper.FindType explicitly does a type search + MethodInfo findTypeTarget = AccessTools.DeclaredMethod(typeof(TypeHelper), nameof(TypeHelper.FindType), new Type[] { typeof(string) }); + MethodInfo findTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(FindTypePostfix)); + harmony.Patch(findTypeTarget, postfix: new HarmonyMethod(findTypePatch)); + + // ReflectionExtensions.IsValidGenericType checks a type for validity, and if it returns `true` it reveals that the type exists + MethodInfo isValidGenericTypeTarget = AccessTools.DeclaredMethod(typeof(ReflectionExtensions), nameof(ReflectionExtensions.IsValidGenericType), new Type[] { typeof(Type), typeof(bool) }); + MethodInfo isValidGenericTypePatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(IsValidTypePostfix)); + harmony.Patch(isValidGenericTypeTarget, postfix: new HarmonyMethod(isValidGenericTypePatch)); + + // FrooxEngine likes to enumerate all types in all assemblies, which is prone to issues (such as crashing FrooxCode if a type isn't loadable) + MethodInfo getAssembliesTarget = AccessTools.DeclaredMethod(typeof(AppDomain), nameof(AppDomain.GetAssemblies), Array.Empty()); + MethodInfo getAssembliesPatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(GetAssembliesPostfix)); + harmony.Patch(getAssembliesTarget, postfix: new HarmonyMethod(getAssembliesPatch)); } private static HashSet GetResoniteAssemblies(HashSet initialAssemblies) { @@ -88,6 +86,9 @@ private static HashSet GetModAssemblies(HashSet resoniteAsse // remove assemblies that we know to have come with Resonite assemblies.ExceptWith(resoniteAssemblies); + // remove ourselves because we technically aren't a mod + assemblies.Remove(Assembly.GetExecutingAssembly()); + // what's left are assemblies that magically appeared during the mod loading process. So mods and their dependencies. return assemblies; } @@ -104,26 +105,24 @@ private static HashSet GetModAssemblies(HashSet resoniteAsse private static bool IsModAssembly(Assembly assembly, string typeOrAssembly, string name, bool log, bool forceShowLate) { if (resoniteAssemblies!.Contains(assembly)) { return false; // The assembly belongs to Resonite and shouldn't be hidden + } else if (modAssemblies!.Contains(assembly)) { + // The assembly belongs to a mod and should be hidden + if (log) { + Logger.DebugFuncInternal(() => $"Hid {typeOrAssembly} \"{name}\" from Resonite"); + } + return true; + } else if (assembly == Assembly.GetExecutingAssembly()) { + // we don't want the data feed components getting hidden + return false; } else { - if (modAssemblies!.Contains(assembly)) { - // The assembly belongs to a mod and should be hidden - if (log) { - Logger.DebugFuncInternal(() => $"Hid {typeOrAssembly} \"{name}\" from Resonite"); - } - return true; - } else { - // an assembly was in neither resoniteAssemblies nor modAssemblies - // this implies someone late-loaded an assembly after RML, and it was later used in-game - // this is super weird, and probably shouldn't ever happen... but if it does, I want to know about it. - // since this is an edge case users may want to handle in different ways, the HideLateTypes rml config option allows them to choose. - //bool hideLate = true;// ModLoaderConfiguration.Get().HideLateTypes; - /*if (log) { - Logger.WarnInternal($"The \"{name}\" {typeOrAssembly} does not appear to part of Resonite or a mod. It is unclear whether it should be hidden or not. Due to the HideLateTypes config option being {hideLate} it will be {(hideLate ? "Hidden" : "Shown")}"); - }*/ - // if forceShowLate == true, then this function will always return `false` for late-loaded types - // if forceShowLate == false, then this function will return `true` when hideLate == true - return !forceShowLate; + // an assembly was in neither resoniteAssemblies nor modAssemblies + // this implies someone late-loaded an assembly after RML, and it was later used in-game + // this is super weird, and probably shouldn't ever happen... but if it does, I want to know about it. + if (log) { + Logger.WarnInternal($"The \"{name}\" {typeOrAssembly} does not appear to part of Resonite or a mod. It is unclear whether it should be hidden or not. forceShowLate is {forceShowLate}, so it will be {(forceShowLate ? "Shown" : "Hidden")}"); } + // if forceShowLate == true, then this function will always return `false` for late-loaded types + return !forceShowLate; } } diff --git a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs index bafd0eb..fd0e5ad 100644 --- a/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs +++ b/ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs @@ -4,4 +4,25 @@ namespace ResoniteModLoader; /// deriving from to be automatically included in that mod's configuration. /// [AttributeUsage(AttributeTargets.Field)] -public sealed class AutoRegisterConfigKeyAttribute : Attribute { } +public sealed class AutoRegisterConfigKeyAttribute : Attribute { + + /// + /// Defines a group that this configuration key belongs to, used by default configuration feed builder. + /// + public string? Group => _group; + + private readonly string? _group; + + /// + /// Flag this field to be automatically registered as a configuration key for this mod that is not grouped with any other keys. + /// + public AutoRegisterConfigKeyAttribute() { } + + /// + /// Flag this field to be automatically registered as a configuration key for this mod that is part of a group. + /// + /// The name of the group this configuration key belongs to. + public AutoRegisterConfigKeyAttribute(string group) { + _group = group; + } +} diff --git a/ResoniteModLoader/DashScreenInjector.cs b/ResoniteModLoader/DashScreenInjector.cs new file mode 100644 index 0000000..22401e9 --- /dev/null +++ b/ResoniteModLoader/DashScreenInjector.cs @@ -0,0 +1,101 @@ +using Elements.Core; +using FrooxEngine; +using FrooxEngine.UIX; +using HarmonyLib; +using Stream = System.IO.Stream; + +namespace ResoniteModLoader; + +internal sealed class DashScreenInjector { + internal static RadiantDashScreen? InjectedScreen; + + internal static void PatchScreenManager(Harmony harmony) { + MethodInfo setupDefaultMethod = AccessTools.DeclaredMethod(typeof(UserspaceScreensManager), "SetupDefaults"); + MethodInfo onLoadingMethod = AccessTools.DeclaredMethod(typeof(UserspaceScreensManager), "OnLoading"); + MethodInfo tryInjectScreenMethod = AccessTools.DeclaredMethod(typeof(DashScreenInjector), nameof(TryInjectScreen)); + harmony.Patch(setupDefaultMethod, postfix: new HarmonyMethod(tryInjectScreenMethod)); + harmony.Patch(onLoadingMethod, postfix: new HarmonyMethod(tryInjectScreenMethod)); + Logger.DebugInternal("UserspaceScreensManager patched"); + } + + internal static async void TryInjectScreen(UserspaceScreensManager __instance) { + ModLoaderConfiguration config = ModLoaderConfiguration.Get(); + + if (config.NoDashScreen) { + Logger.DebugInternal("Dash screen will not be injected due to configuration file"); + return; + } + if (__instance.World != Userspace.UserspaceWorld) { + Logger.WarnInternal("Dash screen will not be injected because we're somehow not in userspace (WTF?)"); // it stands for What the Froox :> + return; + } + if (InjectedScreen is not null && !InjectedScreen.IsRemoved) { + Logger.WarnInternal("Dash screen will not be injected again because it already exists"); + return; + } + + string? overrideTemplatePath = Directory.EnumerateFiles(ModLoaderConfiguration.GetAssemblyDirectory(), "DashScreenTemplate.*").FirstOrDefault(); + Stream screenFileStream = File.Exists(overrideTemplatePath) ? File.OpenRead(overrideTemplatePath) : Assembly.GetExecutingAssembly().GetManifestResourceStream("Resources\\DashScreenTemplate.bson"); + + Logger.DebugInternal("Injecting dash screen"); + + RadiantDash dash = __instance.Slot.GetComponentInParents(); + InjectedScreen = dash.AttachScreen("Mods", RadiantUI_Constants.Neutrals.LIGHT, OfficialAssets.Graphics.Icons.Dash.Tools); + + InjectedScreen.Slot.OrderOffset = 128; + InjectedScreen.Slot.PersistentSelf = false; + + try { + DataTreeDictionary screenDict = DataTreeConverter.LoadAuto(screenFileStream); + Slot DashScreenTemplate = InjectedScreen.ScreenRoot.AddSlot("DashScreenTemplate"); + DashScreenTemplate.LoadObject(screenDict, null!); + Canvas DashScreenCanvas = DashScreenTemplate.GetComponentInChildren(); + InjectedScreen.ScreenCanvas.Slot.AttachComponent().TargetCanvas.Target = DashScreenCanvas; + } + catch (ArgumentNullException e) { + Logger.WarnInternal( + "Failed to load DashScreenTemplate for dash screen, falling back to platform settings."); + Logger.DebugInternal($"Exception message: {e.Message}"); + + SingleFeedView view = InjectedScreen.ScreenRoot.AttachComponent(); + ModConfigurationDataFeed feed = InjectedScreen.ScreenRoot.AttachComponent(); + + Slot templates = InjectedScreen.ScreenRoot.AddSlot("Template"); + templates.ActiveSelf = false; + + if (await templates.LoadObjectAsync(__instance.Cloud.Platform.GetSpawnObjectUri("Settings"), skipHolder: true)) { + // we do a little bit of thievery + RootCategoryView rootCategoryView = templates.GetComponentInChildren(); + rootCategoryView.Slot.GetComponentInChildren().Path.Target = view.Path; + rootCategoryView.CategoryManager.ContainerRoot.Target.ActiveSelf = false; + rootCategoryView.Slot.Children.First().Parent = InjectedScreen.ScreenCanvas.Slot; + view.ItemsManager.TemplateMapper.Target = rootCategoryView.ItemsManager.TemplateMapper.Target; + view.ItemsManager.ContainerRoot.Target = rootCategoryView.ItemsManager.ContainerRoot.Target; + rootCategoryView.Destroy(); + templates.GetComponentInChildren().NameConverter.Target = view.PathSegmentName; + } + else if (config.Debug) { + Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, falling back to template."); + DataFeedItemMapper itemMapper = templates.AttachComponent(); + Canvas tempCanvas = templates.AttachComponent(); // Needed for next method to work + itemMapper.SetupTemplate(); + tempCanvas.Destroy(); + view.ItemsManager.TemplateMapper.Target = itemMapper; + view.ItemsManager.ContainerRoot.Target = InjectedScreen.ScreenCanvas.Slot; + InjectedScreen.ScreenCanvas.Slot.AttachComponent(); // just for debugging + } + else { + Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, aborting and cleaning up."); + InjectedScreen.Slot.Destroy(); + return; + } + + view.Feed.Target = feed; + view.SetCategoryPath(["ResoniteModLoader"]); + } + + InjectedScreen.ScreenCanvas.Slot.AttachComponent().Tint.Value = UserspaceRadiantDash.DEFAULT_BACKGROUND; + + Logger.DebugInternal("Dash screen should be injected!"); + } +} diff --git a/ResoniteModLoader/DebugInfo.cs b/ResoniteModLoader/DebugInfo.cs index db7ec8d..b86b90f 100644 --- a/ResoniteModLoader/DebugInfo.cs +++ b/ResoniteModLoader/DebugInfo.cs @@ -3,6 +3,8 @@ namespace ResoniteModLoader; internal static class DebugInfo { + internal static TimeSpan InitializationTime; + internal static void Log() { Logger.MsgInternal($"ResoniteModLoader v{ModLoader.VERSION} starting up!{(ModLoaderConfiguration.Get().Debug ? " Debug logs will be shown." : "")}"); Logger.DebugFuncInternal(() => $"Launched with args: {string.Join(" ", Environment.GetCommandLineArgs())}"); diff --git a/ResoniteModLoader/ExceptionHook.cs b/ResoniteModLoader/ExceptionHook.cs new file mode 100644 index 0000000..094217c --- /dev/null +++ b/ResoniteModLoader/ExceptionHook.cs @@ -0,0 +1,24 @@ +using FrooxEngine; +using HarmonyLib; + +namespace ResoniteModLoader; + +internal sealed class ExceptionHook { + internal static void RegisterExceptionHook(Harmony harmony) { + MethodInfo? preprocessExceptionMethod = typeof(DebugManager).GetMethod(nameof(DebugManager.PreprocessException), BindingFlags.Static | BindingFlags.Public)?.MakeGenericMethod(typeof(Exception)); + MethodInfo? exceptionHookMethod = typeof(ExceptionHook).GetMethod(nameof(InterceptException), BindingFlags.Static); + + if (preprocessExceptionMethod == null || exceptionHookMethod == null) { + Logger.ErrorInternal("Could not find DebugManager.PreprocessException. Mod exceptions may not be logged."); + return; + } + + harmony.Patch(preprocessExceptionMethod, prefix: new HarmonyMethod(exceptionHookMethod)); + Logger.DebugInternal("DebugManager.PreprocessException patched"); + } + + internal static void InterceptException(Exception exception) { + Logger.ProcessException(exception); + } + +} diff --git a/ResoniteModLoader/ExecutionHook.cs b/ResoniteModLoader/ExecutionHook.cs index 3757107..44cb137 100644 --- a/ResoniteModLoader/ExecutionHook.cs +++ b/ResoniteModLoader/ExecutionHook.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using FrooxEngine; namespace ResoniteModLoader; @@ -18,6 +19,8 @@ private static DummyConnector InstantiateConnector() { static ExecutionHook() { Logger.DebugInternal($"Start of ExecutionHook"); try { + Stopwatch timer = Stopwatch.StartNew(); + Logger.RegisterExceptionHook(); BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic; var byName = (Dictionary)typeof(GlobalTypeRegistry).GetField("_byName", flags).GetValue(null); @@ -39,8 +42,12 @@ static ExecutionHook() { LoadProgressIndicator.SetCustom("Initializing"); DebugInfo.Log(); HarmonyWorker.LoadModsAndHideModAssemblies(initialAssemblies); + timer.Stop(); LoadProgressIndicator.SetCustom("Loaded"); + Logger.MsgInternal($"Initialization & mod loading completed in {timer.ElapsedMilliseconds}ms."); + DebugInfo.InitializationTime = timer.Elapsed; } catch (Exception e) { + Logger.UnregisterExceptionHook(); // it's important that this doesn't send exceptions back to Resonite Logger.ErrorInternal($"Exception in execution hook!\n{e}"); } diff --git a/ResoniteModLoader/HarmonyWorker.cs b/ResoniteModLoader/HarmonyWorker.cs index c19cacb..43df723 100644 --- a/ResoniteModLoader/HarmonyWorker.cs +++ b/ResoniteModLoader/HarmonyWorker.cs @@ -6,8 +6,10 @@ namespace ResoniteModLoader; internal sealed class HarmonyWorker { internal static void LoadModsAndHideModAssemblies(HashSet initialAssemblies) { Harmony harmony = new("com.resonitemodloader.ResoniteModLoader"); + // ExceptionHook.RegisterExceptionHook(harmony); ModLoader.LoadMods(); ModConfiguration.RegisterShutdownHook(harmony); AssemblyHider.PatchResonite(harmony, initialAssemblies); + DashScreenInjector.PatchScreenManager(harmony); } } diff --git a/ResoniteModLoader/Logger.cs b/ResoniteModLoader/Logger.cs index 051c7c2..f541658 100644 --- a/ResoniteModLoader/Logger.cs +++ b/ResoniteModLoader/Logger.cs @@ -1,85 +1,296 @@ +using System.Collections; using System.Diagnostics; - +using System.Runtime.ExceptionServices; using Elements.Core; namespace ResoniteModLoader; -internal sealed class Logger { +/// +/// General class that manages all RML-related log/exception processing. +/// Use the inherited methods from the class instead of UniLog or calling these methods directly! +/// Generally, you will only use this class to read RML/other mod logs, or directly pass exceptions to RML with . +/// +public sealed class Logger { + + /// + /// Represents the severity level of a log message. + /// + public enum LogLevel { TRACE, DEBUG, INFO, WARN, ERROR } + + /// + /// Represents a single log entry. + /// + public class MessageItem { + internal MessageItem(ResoniteModBase? mod, LogLevel level, string message, StackTrace? trace = null) { + Time = DateTime.Now; + Mod = mod; + Level = level; + Message = message; + Trace = trace; + } + + public DateTime Time { get; } + + /// + /// The mod that created this log entry, or RML if null. + /// + public ResoniteModBase? Mod { get; } + public LogLevel Level { get; } + public string Message { get; } + + /// + /// A stack trace relating to the log entry, if recorded. + /// + public StackTrace? Trace { get; } + + /// + public override string ToString() => $"({Mod?.Name ?? "ResoniteModLoader"} @ {Time}) {LogTypeTag(Level)} {Message}"; + } + + /// + /// Represents an exception that was caught or passed for logging. + /// + public class ExceptionItem { + internal ExceptionItem(Exception exception) { + Time = DateTime.Now; + Exception = exception; + } + + internal ExceptionItem(Exception exception, Assembly? assembly) { + Time = DateTime.Now; + Exception = exception; + Source = (assembly, null); + } + + internal ExceptionItem(Exception exception, ResoniteModBase? mod) { + Time = DateTime.Now; + Exception = exception; + Source = (mod?.ModAssembly?.Assembly, mod); + } + + internal ExceptionItem(Exception exception, Assembly? assembly, ResoniteModBase? mod) { + Time = DateTime.Now; + Exception = exception; + Source = (assembly, mod); + } + + public DateTime Time { get; } + public Exception Exception { get; } + + /// + /// The (possible) source of the exception. Note the assembly and mod may be unrelated if both set! + /// + public (Assembly? Assembly, ResoniteModBase? Mod)? Source { get; } + + /// + public override string ToString() => $"({Time}) [{Source?.Assembly?.FullName} ?? Unknown assembly] {Exception.Message}\n{Exception.StackTrace}"; + } + // logged for null objects internal const string NULL_STRING = "null"; + private static List _logBuffer = new(); + + /// + /// Stores all logs posted by mods and RML itself. + /// + public static IReadOnlyList Logs => _logBuffer.AsReadOnly(); + + private static List _exceptionBuffer = new(); + + /// + /// Stores all exceptions caught by RML or passed by mods for logging. + /// + public static IReadOnlyList Exceptions => _exceptionBuffer.AsReadOnly(); + + public delegate void MessageHandler(MessageItem message); + + /// + /// Fired whenever a message is logged. + /// + public static event MessageHandler? OnMessagePosted; + + public delegate void ExceptionHandler(ExceptionItem exception); + + /// + /// Fired whenever an exception is caught by RML or passed by a mod. + /// + public static event ExceptionHandler? OnExceptionPosted; + internal static bool IsDebugEnabled() { return ModLoaderConfiguration.Get().Debug; } + internal static void TraceFuncInternal(Func messageProducer) { + if (IsDebugEnabled()) { + LogInternal(LogLevel.TRACE, messageProducer(), null, true); + } + } + + internal static void TraceFuncExternal(Func messageProducer) { + if (IsDebugEnabled()) { + LogInternal(LogLevel.TRACE, messageProducer(), new(1), true); + } + } + + internal static void TraceInternal(string message) { + if (IsDebugEnabled()) { + LogInternal(LogLevel.TRACE, message, null, true); + } + } + + internal static void TraceExternal(object message) { + if (IsDebugEnabled()) { + LogInternal(LogLevel.TRACE, message, new(1), true); + } + } + + internal static void TraceListExternal(object[] messages) { + if (IsDebugEnabled()) { + LogListInternal(LogLevel.TRACE, messages, new(1), true); + } + } + internal static void DebugFuncInternal(Func messageProducer) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, messageProducer()); + LogInternal(LogLevel.DEBUG, messageProducer()); } } internal static void DebugFuncExternal(Func messageProducer) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, messageProducer(), SourceFromStackTrace(new(1))); + LogInternal(LogLevel.DEBUG, messageProducer(), new(1)); } } internal static void DebugInternal(string message) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, message); + LogInternal(LogLevel.DEBUG, message); } } internal static void DebugExternal(object message) { if (IsDebugEnabled()) { - LogInternal(LogType.DEBUG, message, SourceFromStackTrace(new(1))); + LogInternal(LogLevel.DEBUG, message, new(1)); } } internal static void DebugListExternal(object[] messages) { if (IsDebugEnabled()) { - LogListInternal(LogType.DEBUG, messages, SourceFromStackTrace(new(1))); + LogListInternal(LogLevel.DEBUG, messages, new(1)); } } - internal static void MsgInternal(string message) => LogInternal(LogType.INFO, message); - internal static void MsgExternal(object message) => LogInternal(LogType.INFO, message, SourceFromStackTrace(new(1))); - internal static void MsgListExternal(object[] messages) => LogListInternal(LogType.INFO, messages, SourceFromStackTrace(new(1))); - internal static void WarnInternal(string message) => LogInternal(LogType.WARN, message); - internal static void WarnExternal(object message) => LogInternal(LogType.WARN, message, SourceFromStackTrace(new(1))); - internal static void WarnListExternal(object[] messages) => LogListInternal(LogType.WARN, messages, SourceFromStackTrace(new(1))); - internal static void ErrorInternal(string message) => LogInternal(LogType.ERROR, message); - internal static void ErrorExternal(object message) => LogInternal(LogType.ERROR, message, SourceFromStackTrace(new(1))); - internal static void ErrorListExternal(object[] messages) => LogListInternal(LogType.ERROR, messages, SourceFromStackTrace(new(1))); + internal static void MsgInternal(string message) => LogInternal(LogLevel.INFO, message); + internal static void MsgExternal(object message) => LogInternal(LogLevel.INFO, message, new(1)); + internal static void MsgListExternal(object[] messages) => LogListInternal(LogLevel.INFO, messages, new(1)); + internal static void WarnInternal(string message) => LogInternal(LogLevel.WARN, message); + internal static void WarnExternal(object message) => LogInternal(LogLevel.WARN, message, new(1)); + internal static void WarnListExternal(object[] messages) => LogListInternal(LogLevel.WARN, messages, new(1)); + internal static void ErrorInternal(string message) => LogInternal(LogLevel.ERROR, message); + internal static void ErrorExternal(object message) => LogInternal(LogLevel.ERROR, message, new(1)); + internal static void ErrorListExternal(object[] messages) => LogListInternal(LogLevel.ERROR, messages, new(1)); - private static void LogInternal(string logTypePrefix, object message, string? source = null) { + private static void LogInternal(LogLevel logType, object message, StackTrace? stackTrace = null, bool includeTrace = false) { message ??= NULL_STRING; + stackTrace ??= new(1); + ResoniteMod? source = Util.ExecutingMod(stackTrace); + string logTypePrefix = LogTypeTag(logType); + MessageItem item = new(source, logType, message.ToString(), stackTrace); + _logBuffer.Add(item); + OnMessagePosted?.SafeInvoke(item); if (source == null) { - UniLog.Log($"{logTypePrefix}[ResoniteModLoader] {message}"); - } else { - UniLog.Log($"{logTypePrefix}[ResoniteModLoader/{source}] {message}"); + UniLog.Log($"{logTypePrefix}[ResoniteModLoader] {message}", includeTrace); + } + else { + UniLog.Log($"{logTypePrefix}[ResoniteModLoader/{source.Name}] {message}", includeTrace); } } - private static void LogListInternal(string logTypePrefix, object[] messages, string? source) { + private static void LogListInternal(LogLevel logType, object[] messages, StackTrace? stackTrace, bool includeTrace = false) { if (messages == null) { - LogInternal(logTypePrefix, NULL_STRING, source); - } else { + LogInternal(logType, NULL_STRING, stackTrace, includeTrace); + } + else { foreach (object element in messages) { - LogInternal(logTypePrefix, element.ToString(), source); + LogInternal(logType, element.ToString(), stackTrace, includeTrace); } } } - private static string? SourceFromStackTrace(StackTrace stackTrace) { - // MsgExternal() and Msg() are above us in the stack - return Util.ExecutingMod(stackTrace)?.Name; + /// + /// Use to pass a caught exception to RML for logging purposes. + /// Note that calling this will not automatically produce an error message, unless debug is enabled in RML's config. + /// + /// The exception to be recorded + /// The assembly responsible for causing the exception, if known + /// The mod where the exception occurred, if known + public static void ProcessException(Exception exception, Assembly? assembly = null, ResoniteModBase? mod = null) { + ExceptionItem item = new(exception, assembly, mod); + _exceptionBuffer.Add(item); + OnExceptionPosted?.SafeInvoke(item); + if (IsDebugEnabled()) { + string? attribution = null; + attribution ??= mod?.Name; + attribution ??= assembly?.FullName; + attribution ??= "unknown mod/assembly"; + LogInternal(LogLevel.ERROR, $"DEBUG EXCEPTION [{attribution}]: {exception.Message}", new StackTrace(exception), true); + } + } + + private static string LogTypeTag(LogLevel logType) => $"[{Enum.GetName(typeof(LogLevel), logType)}]"; + + internal static void RegisterExceptionHook() { + // AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionProcessor; + AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionProcessor; + DebugInternal("Unhandled exception hook registered"); + } + + internal static void UnregisterExceptionHook() { + // AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionProcessor; + AppDomain.CurrentDomain.FirstChanceException -= FirstChanceExceptionProcessor; + DebugInternal("Unhandled exception hook unregistered"); } - private static class LogType { - internal const string DEBUG = "[DEBUG]"; - internal const string INFO = "[INFO] "; - internal const string WARN = "[WARN] "; - internal const string ERROR = "[ERROR]"; + private static void UnhandledExceptionProcessor(object sender, UnhandledExceptionEventArgs args) { + Exception exception = (Exception)args.ExceptionObject; + StackTrace trace = new StackTrace(exception); + ResoniteModBase? mod = Util.ExecutingMod(trace); + Assembly assembly = Assembly.GetAssembly(sender.GetType()); + // this should handle most uncaught cases in RML and mods + if (mod is not null || assembly == Assembly.GetExecutingAssembly()) { + if (IsDebugEnabled()) ErrorInternal($"Caught unhandled exception, {exception.Message}. Attributed to {mod?.Name ?? "No mod"} / {assembly.FullName}"); + ProcessException(exception, assembly, mod); + } + + } private static void FirstChanceExceptionProcessor(object sender, FirstChanceExceptionEventArgs args) { + Exception exception = args.Exception; + StackTrace trace = new StackTrace(exception); + ResoniteModBase? mod = Util.ExecutingMod(trace); + Assembly assembly = Assembly.GetAssembly(sender.GetType()); + // this should handle most uncaught cases in RML and mods + if (mod is not null || assembly == Assembly.GetExecutingAssembly()) { + if (IsDebugEnabled()) ErrorInternal($"Caught mod-related exception, {exception.Message}. Attributed to {mod?.Name ?? "No mod"} / {assembly.FullName}"); + ProcessException(exception, assembly, mod); + } } } + +/// +/// Extension methods to filter logs/exceptions from a single mod. +/// +public static class LoggerExtensions { + /// + /// Gets messages that were logged by this mod. + /// + /// The mod to filter messages from + /// Any messages logged by this mod. + public static IEnumerable Logs(this ResoniteModBase mod) => Logger.Logs.Where((line) => line.Mod == mod); + + /// + /// Gets exceptions that are related to this mod. + /// + /// The mod to filter exceptions on + /// Any exceptions related to this mod. + public static IEnumerable Exceptions(this ResoniteModBase mod) => Logger.Exceptions.Where((line) => line.Source?.Mod == mod); +} diff --git a/ResoniteModLoader/ModConfiguration.cs b/ResoniteModLoader/ModConfiguration.cs index 3f8442a..8ae4066 100644 --- a/ResoniteModLoader/ModConfiguration.cs +++ b/ResoniteModLoader/ModConfiguration.cs @@ -134,6 +134,9 @@ public class ModConfiguration : IModConfigurationDefinition { // The naughty list is global, while the actual debouncing is per-configuration. private static HashSet naughtySavers = new HashSet(); + // maps configs by their filename sans extensions + internal static Dictionary configNameMap = new Dictionary(); + // used to keep track of the debouncers for this configuration. private Dictionary> saveActionForCallee = new(); @@ -160,6 +163,8 @@ private static JsonSerializer CreateJsonSerializer() { private ModConfiguration(ModConfigurationDefinition definition) { Definition = definition; + configNameMap[Path.GetFileNameWithoutExtension(Owner.ModAssembly!.File)] = this; + // thank goodness ModAssembly is set literally right before this is created } internal static void EnsureDirectoryExists() { diff --git a/ResoniteModLoader/ModConfigurationDataFeed.cs b/ResoniteModLoader/ModConfigurationDataFeed.cs new file mode 100644 index 0000000..a5f5af3 --- /dev/null +++ b/ResoniteModLoader/ModConfigurationDataFeed.cs @@ -0,0 +1,299 @@ +using Elements.Core; +using FrooxEngine; + +namespace ResoniteModLoader; + +/// +/// A custom data feed that can be used to show information about loaded mods, and alter their configuration. Path must start with "ResoniteModLoader" +/// +[Category(["ResoniteModLoader"])] +public class ModConfigurationDataFeed : Component, IDataFeedComponent, IDataFeed, IWorldElement { +#pragma warning disable CS1591 +#if !DEBUG + public override bool UserspaceOnly => true; +#endif + + public bool SupportsBackgroundQuerying => true; +#pragma warning restore CS1591 +#pragma warning disable CS8618, CA1051 // FrooxEngine weaver will take care of these + /// + /// Show mod configuration keys marked as internal. + /// + public readonly Sync IncludeInternalConfigItems; + + /// + /// Enable or disable the use of custom configuration feeds. + /// + public readonly Sync IgnoreModDefinedEnumerate; +#pragma warning restore CS8618, CA1051 +#pragma warning disable CS1591 + public async IAsyncEnumerable Enumerate(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData) { + switch (path.Count) { + case 0: { + yield return FeedBuilder.Category("ResoniteModLoader", "ResoniteModLoader (Home page)"); + foreach (ResoniteModBase mod in ModLoader.Mods()) + yield return FeedBuilder.Category(KeyFromMod(mod), mod.Name, ["ResoniteModLoader", KeyFromMod(mod)]); + } + yield break; + + case 1: { + if (path[0] != "ResoniteModLoader") yield break; + + if (string.IsNullOrEmpty(searchPhrase)) { + yield return FeedBuilder.Group("ResoniteModLoader.InfoGroup", "RML", [ + FeedBuilder.Label("ResoniteModLoader.Version", $"ResoniteModLoader version {ModLoader.VERSION}"), + #if DEBUG + FeedBuilder.Label("ResoniteModLoader.DebugBuild", "/!\\ RUNNING DEBUG BUILD CONFIGURATION /!\\", RadiantUI_Constants.Hero.ORANGE), + #endif + FeedBuilder.StringIndicator("ResoniteModLoader.LoadedModCount", "Loaded mods", ModLoader.Mods().Count()), + FeedBuilder.StringIndicator("ResoniteModLoader.InitializationTime", "Startup time", DebugInfo.InitializationTime.Milliseconds + "ms") + ]); + + List modCategories = new(); + foreach (ResoniteModBase mod in ModLoader.Mods()) + modCategories.Add(FeedBuilder.Category(KeyFromMod(mod), mod.Name)); + + yield return FeedBuilder.Grid("ResoniteModLoader.ModsGroup", "Mods", modCategories); + } + else { + List filteredMods = ModLoader.Mods().Where((mod) => mod.Name.IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) >= 0).ToList(); + yield return FeedBuilder.Label("ResoniteModLoader.SearchResults", filteredMods.Any() ? $"Search results: {filteredMods.Count} mods found." : "No results!"); + + foreach (ResoniteModBase mod in filteredMods) + yield return mod.GenerateModInfoGroup(false); + } + } + yield break; + + case 2: { + if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; + + string key = KeyFromMod(mod); + yield return mod.GenerateModInfoGroup(true); + + List modLogs = mod.Logs().ToList(); + if (modLogs.Any()) { + IReadOnlyList latestLogs = mod.GenerateModLogFeed(5, false).Append(FeedBuilder.Category("Logs", $"View full log ({modLogs.Count})")).ToList().AsReadOnly(); + yield return FeedBuilder.Group(key + ".Logs", "Recent mod logs", latestLogs); + } + + List modExceptions = mod.Exceptions().ToList(); + if (modExceptions.Any()) { + IReadOnlyList latestException = mod.GenerateModExceptionFeed(1, false).Append(FeedBuilder.Category("Exceptions", $"View all exceptions ({modExceptions.Count})")).ToList().AsReadOnly(); + yield return FeedBuilder.Group(key + ".Exceptions", "Latest mod exception", latestException); + } + } + yield break; + + case 3: { + if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; + + switch (path[2].ToLower()) { + case "configuration": { + foreach (DataFeedItem item in mod.GenerateModConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value, IgnoreModDefinedEnumerate.Value)) + yield return item; + } + yield break; + + case "logs": { + foreach (DataFeedItem item in mod.GenerateModLogFeed(int.MaxValue, true, searchPhrase)) + yield return item; + } + yield break; + + case "exceptions": { + foreach (DataFeedItem item in mod.GenerateModExceptionFeed(int.MaxValue, true, searchPhrase)) + yield return item; + } + yield break; + + default: { + // Reserved for future use - mods defining their own subfeeds + } + yield break; + } + } + case > 3: { + if (path[0] != "ResoniteModLoader" || !TryModFromKey(path[1], out var mod)) yield break; + + if (path[2].ToLower() == "configuration") { + foreach (DataFeedItem item in mod.GenerateModConfigurationFeed(path.Skip(3).ToArray(), groupKeys, searchPhrase, viewData, IncludeInternalConfigItems.Value, IgnoreModDefinedEnumerate.Value)) + yield return item; + + yield break; + } + else { + // Reserved for future use - mods defining their own subfeeds + } + } + yield break; + } + } + + public void ListenToUpdates(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler, object viewData) { + Logger.DebugInternal($"ModConfigurationDataFeed.ListenToUpdates called, handler: {handler}\n{Environment.StackTrace}"); + } + + public LocaleString PathSegmentName(string segment, int depth) { + return depth switch { + 2 => ModFromKey(segment)?.Name ?? "INVALID", + 3 => segment.Capitalize(), + _ => segment + }; + } + + public object RegisterViewData() { + Logger.DebugInternal($"ModConfigurationDataFeed.RegisterViewData called\n{Environment.StackTrace}"); + return this; + } + + public void UnregisterListener(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, DataFeedUpdateHandler handler) { + Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterListener called, handler: {handler}\n{Environment.StackTrace}"); + } + + public void UnregisterViewData(object data) { + Logger.DebugInternal($"ModConfigurationDataFeed.UnregisterViewData called, object: {data}\n{Environment.StackTrace}"); + } +#pragma warning restore CS1591 + /// + /// Returns a unique key that can be used to reference this mod. + /// + /// The mod to return a key from. + /// A unique key representing the mod. + /// + /// + internal static string KeyFromMod(ResoniteModBase mod) => Path.GetFileNameWithoutExtension(mod.ModAssembly!.File); + + /// + /// Returns the mod that corresponds to a unique key. + /// + /// A unique key from . + /// The mod that corresponds with the unique key, or null if one couldn't be found. + /// + internal static ResoniteModBase? ModFromKey(string key) => ModLoader.Mods().First((mod) => KeyFromMod(mod) == key); + + /// + /// Tries to get the mod that corresponds to a unique key. + /// + /// A unique key from . + /// Set if a matching mod is found. + /// true if a matching mod is found, false otherwise. + internal static bool TryModFromKey(string key, out ResoniteModBase mod) { + mod = ModFromKey(key)!; + return mod is not null; + } +} + +internal static class ModConfigurationDataFeedExtensions { + /// + /// Generates a DataFeedGroup that displays basic information about a mod. + /// + /// The target mod + /// Set to true if this group will be displayed on its own page + /// A group containing indicators for the mod's info, as well as categories to view its config/logs/exceptions. + internal static DataFeedGroup GenerateModInfoGroup(this ResoniteModBase mod, bool standalone) { + List groupChildren = new(); + string key = ModConfigurationDataFeed.KeyFromMod(mod); + + if (standalone) + groupChildren.Add(FeedBuilder.Indicator(key + ".Name", "Name", mod.Name)); + groupChildren.Add(FeedBuilder.Indicator(key + ".Author", "Author", mod.Author)); + groupChildren.Add(FeedBuilder.Indicator(key + ".Version", "Version", mod.Version)); + + if (standalone) { + groupChildren.Add(FeedBuilder.StringIndicator(key + ".InitializationTime", "Startup impact", mod.InitializationTime.Milliseconds + "ms")); + groupChildren.Add(FeedBuilder.Indicator(key + ".AssemblyFile", "Assembly file", Path.GetFileName(mod.ModAssembly!.File))); + if (Logger.IsDebugEnabled()) + groupChildren.Add(FeedBuilder.Indicator(key + ".AssemblyHash", "Assembly hash", mod.ModAssembly!.Sha256)); + } + + if (Uri.TryCreate(mod.Link, UriKind.Absolute, out var uri)) + groupChildren.Add(FeedBuilder.ValueAction(key + ".OpenLinkAction", $"Open mod link ({uri.Host})", (action) => action.Target = OpenURI, uri)); + if (mod.GetConfiguration() is not null) + groupChildren.Add(FeedBuilder.Category(key + ".ConfigurationCategory", "Mod configuration", standalone ? ["Configuration"] : [key, "Configuration"])); + + if (!standalone) { + List modLogs = mod.Logs().ToList(); + List modExceptions = mod.Exceptions().ToList(); + if (modLogs.Any()) + groupChildren.Add(FeedBuilder.Category(key + ".LogsCategory", $"Mod logs ({modLogs.Count})", [key, "Logs"])); + if (modExceptions.Any()) + groupChildren.Add(FeedBuilder.Category(key + ".ExceptionsCategory", $"Mod exceptions ({modExceptions.Count})", [key, "Exceptions"])); + } + + return FeedBuilder.Group(key + ".Group", standalone ? "Mod info" : mod.Name, groupChildren); + } + + internal static IEnumerable GenerateModConfigurationFeed(this ResoniteModBase mod, IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false, bool forceDefaultBuilder = false) { + if (path.Any() && path[0] == "ResoniteModLoader") + Logger.WarnInternal("Call to GenerateModConfigurationFeed may include full DataFeed path, if so expect broken behavior."); + + if (!mod.TryGetConfiguration(out ModConfiguration config) || !config.ConfigurationItemDefinitions.Any()) { + yield return FeedBuilder.Label("NoConfig", "This mod does not define any configuration keys.", color.Red); + yield break; + } + + List items = new(); + bool failed = false; + + if (!forceDefaultBuilder) { + try { + items = mod.BuildConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal).ToList(); + } + catch (Exception ex) { + failed = true; + Logger.ProcessException(ex, mod.ModAssembly!.Assembly); + Logger.ErrorInternal($"Exception was thrown while running {mod.Name}'s BuildConfigurationFeed method"); + } + } + + if (failed || !items.Any()) { + ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); + builder ??= new ModConfigurationFeedBuilder(config); + items = builder.RootPage(searchPhrase, includeInternal).ToList(); + } + + Logger.DebugInternal($"GenerateModConfigurationFeed output for {mod.Name} @ {string.Join("/", path)}"); + foreach (DataFeedItem item in items) { + Logger.DebugInternal($"\t{item.GetType().Name} : {item.ItemKey}"); + yield return item; + } + } + + private static DataFeedItem AsFeedItem(this string text, bool copyable = true, string? key = null) { + key ??= Guid.NewGuid().ToString(); + if (copyable) + return FeedBuilder.ValueAction(key, text, (action) => action.Target = CopyText, text); + else + return FeedBuilder.Label(key, text); + } + + internal static IEnumerable GenerateModLogFeed(this ResoniteModBase mod, int last = int.MaxValue, bool copyable = true, string? filter = null) { + List modLogs = mod.Logs().Reverse().Take(last).ToList(); + if (!string.IsNullOrEmpty(filter)) + modLogs = modLogs.Where((line) => line.Message.IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0).ToList(); + foreach (Logger.MessageItem line in modLogs) + yield return line.ToString().AsFeedItem(copyable); + } + + internal static IEnumerable GenerateModExceptionFeed(this ResoniteModBase mod, int last = int.MaxValue, bool copyable = true, string? filter = null) { + List modExceptions = mod.Exceptions().Reverse().Take(last).ToList(); + if (!string.IsNullOrEmpty(filter)) + modExceptions = modExceptions.Where((line) => line.Exception.ToString().IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) >= 0).ToList(); + foreach (Logger.ExceptionItem line in modExceptions) + yield return line.ToString().AsFeedItem(copyable); + } + + [SyncMethod(typeof(Action), [])] + private static void OpenURI(Uri uri) { + Slot slot = Userspace.UserspaceWorld.AddSlot("Hyperlink"); + slot.PositionInFrontOfUser(float3.Backward); + slot.AttachComponent().Setup(uri, "Outgoing hyperlink"); + } + + [SyncMethod(typeof(Action), [])] + private static void CopyText(string text) { + Userspace.UserspaceWorld.InputInterface.Clipboard.SetText(text); + NotificationMessage.SpawnTextMessage("Copied line", colorX.White); + } +} diff --git a/ResoniteModLoader/ModConfigurationFeedBuilder.cs b/ResoniteModLoader/ModConfigurationFeedBuilder.cs new file mode 100644 index 0000000..6044855 --- /dev/null +++ b/ResoniteModLoader/ModConfigurationFeedBuilder.cs @@ -0,0 +1,303 @@ +using Elements.Core; +using FrooxEngine; +using HarmonyLib; +using System.Collections; + +namespace ResoniteModLoader; + +/// +/// A utility class that aids in the creation of mod configuration feeds. +/// +public class ModConfigurationFeedBuilder { + /// + /// A cache of , indexed by the they belong to. + /// New builders are automatically added to this cache upon instantiation, so you should try to get a cached builder before creating a new one. + /// + /// + /// + /// ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); + /// builder ??= new ModConfigurationFeedBuilder(config); + /// + /// + public readonly static Dictionary CachedBuilders = new(); + + private readonly ModConfiguration Config; + + private readonly Dictionary KeyFields = new(); + + private readonly Dictionary> KeyGrouping = new(); + + private static bool HasAutoRegisterAttribute(FieldInfo field) => field.GetCustomAttribute() is not null; + + private static bool TryGetAutoRegisterAttribute(FieldInfo field, out AutoRegisterConfigKeyAttribute attribute) { + attribute = field.GetCustomAttribute(); + return attribute is not null; + } + + private static bool HasRangeAttribute(FieldInfo field) => field.GetCustomAttribute() is not null; + + private static bool TryGetRangeAttribute(FieldInfo field, out RangeAttribute attribute) { + attribute = field.GetCustomAttribute(); + return attribute is not null; + } + + private void AssertChildKey(ModConfigurationKey key) { + if (!Config.IsKeyDefined(key)) + throw new InvalidOperationException($"Mod key ({key}) is not owned by {Config.Owner.Name}'s config"); + } + + private static void AssertMatchingType(ModConfigurationKey key) { + if (key.ValueType() != typeof(T)) + throw new InvalidOperationException($"Type of mod key ({key}) does not match field type {typeof(T)}"); + } + + private string GetKeyLabel(ModConfigurationKey key) + => (key.InternalAccessOnly ? "[INTERNAL] " : "") + + (PreferDescriptionLabels ? (key.Description ?? key.Name) : key.Name); + + private string GetKeyDescription(ModConfigurationKey key) + => PreferDescriptionLabels ? $"Key name: {key.Name}" : (key.Description ?? "(No description)"); + + /// + /// If true, configuration key descriptions will be used as the DataFeedItem's label if they exist. + /// If false, the configuration key name will be used as the label. + /// In both cases, the description will be the opposite field of the label. + /// + public bool PreferDescriptionLabels { get; set; } = true; + + /// + /// Instantiates and caches a new builder for a specific . + /// Check if a cached builder exists in before creating a new one! + /// + /// The mod configuration this builder will generate items for + public ModConfigurationFeedBuilder(ModConfiguration config) { + Config = config; + IEnumerable autoConfigKeys = config.Owner.GetType().GetDeclaredFields().Where(HasAutoRegisterAttribute); + HashSet groupedKeys = new(); + + foreach (FieldInfo field in autoConfigKeys) { + ModConfigurationKey key = (ModConfigurationKey)field.GetValue(field.IsStatic ? null : config.Owner); + if (key is null) continue; // dunno why this would happen + KeyFields[key] = field; + + AutoRegisterConfigKeyAttribute attribute = field.GetCustomAttribute(); + if (attribute.Group is string groupName) { + if (!KeyGrouping.ContainsKey(groupName)) + KeyGrouping[groupName] = new(); + KeyGrouping[groupName].Add(key); + groupedKeys.Add(key); + } + } + + if (groupedKeys.Any()) { + if (!KeyGrouping.ContainsKey("Uncategorized")) + KeyGrouping["Uncategorized"] = new(); + + foreach (ModConfigurationKey key in config.ConfigurationItemDefinitions) + if (!groupedKeys.Contains(key)) + KeyGrouping["Uncategorized"].Add(key); + } + + CachedBuilders[config] = this; + + if (Logger.IsDebugEnabled()) { + Logger.DebugInternal("--- ModConfigurationFeedBuilder instantiated ---"); + Logger.DebugInternal($"Config owner: {config.Owner.Name}"); + Logger.DebugInternal($"Total keys: {config.ConfigurationItemDefinitions.Count}"); + Logger.DebugInternal($"AutoRegistered keys: {autoConfigKeys.Count()}, Grouped: {groupedKeys.Count}"); + Logger.DebugInternal($"Key groups ({KeyGrouping.Keys.Count}): [{string.Join(", ", KeyGrouping.Keys)}]"); + } + } + + public IEnumerable Page(IReadOnlyList path, string searchPhrase = "", bool includeInternal = false) + { + if (path is null || !path.Any()) + foreach (DataFeedItem item in RootPage(searchPhrase, includeInternal)) + yield return item; + + } + + /// + /// Generates a root config page containing all defined config keys. + /// + /// If set, only show keys whose name or description contains this string + /// If true, also generate items for config keys marked as internal + /// Feed items for all defined config keys, plus buttons to save, discard, and reset the config. + public IEnumerable RootPage(string searchPhrase = "", bool includeInternal = false) { + if (KeyGrouping.Any()) { + foreach (string group in KeyGrouping.Keys) { + DataFeedGroup container = FeedBuilder.Group(group, group); + foreach (ModConfigurationKey key in Config.ConfigurationItemDefinitions.Where(KeyGrouping[group].Contains)) { + if (key.InternalAccessOnly && !includeInternal) continue; + if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; + container.AddSubitems(GenerateDataFeedItem(key)); + } + if (container.SubItems?.Any() ?? false) yield return container; + } + } + else { + foreach (ModConfigurationKey key in Config.ConfigurationItemDefinitions) { + if (key.InternalAccessOnly && !includeInternal) continue; + if (!string.IsNullOrEmpty(searchPhrase) && string.Join("\n", key.Name, key.Description).IndexOf(searchPhrase, StringComparison.InvariantCultureIgnoreCase) < 0) continue; + yield return GenerateDataFeedItem(key); + } + } + + yield return GenerateSaveControlButtons(); + } + + /// + /// (NOT YET IMPLEMENTED) Generates a subpage for an indexed/enumerable config key. + /// ie. arrays, lists, dictionaries, sets. + /// + /// A key with an enumerable type + /// If true, items may only be reordered, not added/removed. + /// A ordered feed item for each element in the key's value, plus a group of buttons to add/remove items if set. + private IEnumerable> EnumerablePage(ModConfigurationKey key, bool reorderOnly = false) { + AssertChildKey(key); + if (!typeof(IEnumerable).IsAssignableFrom(key.ValueType())) yield break; + var value = (IEnumerable)Config.GetValue(key); + int i = 0; + foreach (object item in value) + yield return FeedBuilder.OrderedItem(key.Name + i, key.Name, item.ToString(), i++); + if (reorderOnly) yield break; + // Group that contains input field plus buttons to prepend/append, and remove first/last item + } + + // these generate methods need to be cleaned up and more strongly typed + // todo: Make all these methods use generic keys + + /// + /// Generates a slider for the defining key if it is a float has a range attribute, otherwise generates a generic value field. + /// + /// The value type of the supplied key + /// The key to generate the item from + /// A DataFeedSlider if possible, otherwise a DataFeedValueField. + /// + public DataFeedValueField GenerateDataFeedField(ModConfigurationKey key) { + AssertChildKey(key); + AssertMatchingType(key); + string label = GetKeyLabel(key); + string description = GetKeyDescription(key); + if (typeof(T).IsAssignableFrom(typeof(float)) && KeyFields.TryGetValue(key, out FieldInfo field) && TryGetRangeAttribute(field, out RangeAttribute range) && range.Min is T min && range.Max is T max) + return FeedBuilder.Slider(key.Name, label, description, (field) => field.SyncWithModConfiguration(Config, key), min, max, range.TextFormat); + // If range attribute wasn't limited to floats, we could also make ClampedValueField's + else + return FeedBuilder.ValueField(key.Name, label, description, (field) => field.SyncWithModConfiguration(Config, key)); + } + + /// + /// Generates an enum field for a specific configuration key. + /// + /// The enum type of the supplied key + /// The key to generate the item from + /// A physical mango if it is opposite day. + /// + public DataFeedEnum GenerateDataFeedEnum(ModConfigurationKey key) where E : Enum { + AssertChildKey(key); + AssertMatchingType(key); + string label = GetKeyLabel(key); + string description = GetKeyDescription(key); + return FeedBuilder.Enum(key.Name, label, description, (field) => field.SyncWithModConfiguration(Config, key)); + } + + /// + /// Generates the appropriate DataFeedItem for any config key type. + /// + /// The key to generate the item from + /// Automatically picks the best item type for the config key type. + public DataFeedItem GenerateDataFeedItem(ModConfigurationKey key) { + AssertChildKey(key); + string label = GetKeyLabel(key); + string description = GetKeyDescription(key); + Type valueType = key.ValueType(); + if (valueType == typeof(dummy)) + return FeedBuilder.Label(key.Name, label, description); + else if (valueType == typeof(bool)) + return FeedBuilder.Toggle(key.Name, label, description, (field) => field.SyncWithModConfiguration(Config, key)); + else if (valueType != typeof(string) && valueType != typeof(Uri) && typeof(IEnumerable).IsAssignableFrom(valueType)) + return FeedBuilder.Category(key.Name, label, description); + else if (valueType.InheritsFrom(typeof(Enum))) + return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedEnum)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); + else + return (DataFeedItem)typeof(ModConfigurationFeedBuilder).GetMethod(nameof(GenerateDataFeedField)).MakeGenericMethod(key.ValueType()).Invoke(this, [key]); + } + + /// + /// Generates buttons to save/discard changes, or reset all config keys to their defaults. + /// + /// A group with the aforementioned options. + public DataFeedGrid GenerateSaveControlButtons() { + string configName = Path.GetFileNameWithoutExtension(Config.Owner.ModAssembly!.File); + DataFeedGrid container = FeedBuilder.Grid("SaveControlButtonsGrid", "", [ + FeedBuilder.ValueAction("Save", "Save changes", (action) => action.Target = SaveConfig, configName), + FeedBuilder.ValueAction("Discard", "Discard changes", (action) => action.Target = DiscardConfig, configName), + FeedBuilder.ValueAction("Reset", "Reset all options", (action) => action.Target = ResetConfig, configName) + ]); + return container; + } + + [SyncMethod(typeof(Action), [])] + private static void SaveConfig(string configName) { + if (ModConfiguration.configNameMap.TryGetValue(configName, out var config)) { + config.SaveQueue(false, true); + NotificationMessage.SpawnTextMessage("Saved successfully", colorX.White); + } else + NotificationMessage.SpawnTextMessage("Failed to save!", colorX.Red); + } + + [SyncMethod(typeof(Action), [])] + private static void DiscardConfig(string configName) { + Userspace.OpenContextMenu( + Userspace.UserspaceWorld.GetGloballyRegisteredComponent().Slot, + new ContextMenuOptions { disableFlick = true }, + async (menu) => { + menu.AddItem( + "Really discard changes", + OfficialAssets.Graphics.Icons.Inspector.DestroyPreservingAssets, + colorX.Red + ).Button.LocalPressed += (_, _) => { + NotificationMessage.SpawnTextMessage("Not implemented", colorX.Yellow); + menu.Close(); + }; + menu.AddItem("Cancel", (Uri)null!, colorX.White) + .Button.LocalPressed += (_, _) => menu.Close(); + } + ); + } + + [SyncMethod(typeof(Action), [])] + private static void ResetConfig(string configName) { + Userspace.OpenContextMenu( + Userspace.UserspaceWorld.GetGloballyRegisteredComponent().Slot, + new ContextMenuOptions { disableFlick = true }, + async (menu) => { + menu.AddItem( + "Really reset configuration", + OfficialAssets.Graphics.Icons.Inspector.Destroy, + colorX.Red + ).Button.LocalPressed += (_, _) => { + NotificationMessage.SpawnTextMessage("Not implemented", colorX.Yellow); + menu.Close(); + }; + menu.AddItem("Cancel", (Uri)null!, colorX.White) + .Button.LocalPressed += (_, _) => menu.Close(); + } + ); + } +} + +/// +/// Extentions that work with 's +/// +public static class ModConfigurationFeedBuilderExtensions { + /// + /// Returns a cached , or creates a new one. + /// + /// The the builder belongs to + /// A cached or new builder. + public static ModConfigurationFeedBuilder ConfigurationFeedBuilder(this ModConfiguration config) { + ModConfigurationFeedBuilder.CachedBuilders.TryGetValue(config, out var builder); + return builder ?? new ModConfigurationFeedBuilder(config); + } +} diff --git a/ResoniteModLoader/ModConfigurationValueSync.cs b/ResoniteModLoader/ModConfigurationValueSync.cs new file mode 100644 index 0000000..780176a --- /dev/null +++ b/ResoniteModLoader/ModConfigurationValueSync.cs @@ -0,0 +1,155 @@ +using Elements.Core; +using FrooxEngine; + +namespace ResoniteModLoader; + +/// +/// Bi-directionally syncs a field with a specific mod configuration key. +/// +/// The mod configuration key type +[Category(["ResoniteModLoader"])] +public class ModConfigurationValueSync : Component { +#pragma warning disable CS1591 +#if !DEBUG + public override bool UserspaceOnly => true; +#endif +#pragma warning restore CS1591 +#pragma warning disable CS8618, CA1051 + public readonly Sync DefiningModAssembly; + + public readonly Sync ConfigurationKeyName; + + public readonly RawOutput DefinitionFound; + + public readonly FieldDrive TargetField; +#pragma warning restore CS8618, CA1051 + private ResoniteModBase? _mappedMod; + + private ModConfiguration? _mappedConfig; + + private ModConfigurationKey? _mappedKey; + + private bool _definitionFound; +#pragma warning disable CS1591 + protected override void OnAwake() { + base.OnAwake(); + TargetField.SetupValueSetHook((IField field, T value) => { + if (_mappedKey is not null) { + if (_mappedKey.Validate(value)) { + TargetField.Target.Value = value; + _mappedConfig!.Set(_mappedKey, value); + } + } + }); + } + + protected override void OnChanges() { + base.OnChanges(); + Unregister(); + if (MapModConfigKey()) + Register(); + DefinitionFound.Value = _definitionFound; + } + + protected override void OnDispose() { + Unregister(); + base.OnDispose(); + } + + protected override void OnStart() { + base.OnStart(); + if (MapModConfigKey()) + Register(); + } +#pragma warning restore CS1591 + /// + /// Attempts to match the supplied and fields to a mod config and key + /// + /// Success + private bool MapModConfigKey() { + if (string.IsNullOrEmpty(DefiningModAssembly.Value) || string.IsNullOrEmpty(ConfigurationKeyName.Value)) + return false; + try { + _mappedMod = ModLoader.Mods().Single((mod) => Path.GetFileNameWithoutExtension(mod.ModAssembly?.File) == DefiningModAssembly.Value); + _mappedConfig = _mappedMod?.GetConfiguration(); + _mappedKey = _mappedConfig?.ConfigurationItemDefinitions.Single((key) => key.Name == ConfigurationKeyName.Value); + if (_mappedMod is null || _mappedConfig is null || _mappedKey is null) + return false; + return _mappedKey.ValueType() == typeof(T); + } + catch (Exception) { + return false; + } + } + + /// + /// Call AFTER mapping has been confirmed to begin syncing the target field + /// + private void Register() { + ConfigValueChanged(_mappedConfig.GetValue(_mappedKey)); + _mappedKey!.OnChanged += ConfigValueChanged; + _definitionFound = true; + } + + /// + /// Stop syncing, call whenever any field has changed to make sure the rug isn't pulled out from under us. + /// + private void Unregister() { + if (_mappedKey is not null) + _mappedKey.OnChanged -= ConfigValueChanged; + _mappedMod = null; + _mappedConfig = null; + _mappedKey = null; + _definitionFound = false; + } + + private void ConfigValueChanged(object? value) { + if (TargetField.IsLinkValid) + TargetField.Target.Value = (T)value ?? default; + } + + /// + /// Sets the and fields to match the supplied config and key. + /// + /// The configuration the key belongs to + /// Any key with a matching type + public void LoadConfigKey(ModConfiguration config, ModConfigurationKey key) { + if (!config.IsKeyDefined(key)) + throw new InvalidOperationException($"Mod key ({key}) is not owned by {config.Owner.Name}'s config"); + + _mappedMod = config.Owner; + _mappedConfig = config; + _mappedKey = key; + DefiningModAssembly.Value = Path.GetFileNameWithoutExtension(config.Owner.ModAssembly!.File); + ConfigurationKeyName.Value = key.Name; + Register(); + } +} + +/// +/// Utilities methods that attaches to stuff. +/// +public static class ModConfigurationValueSyncExtensions { + /// + /// Syncs a target IField with a mod configuration key. + /// + /// The field and key type + /// The field to bi-directionally sync + /// The configuration the key belongs to + /// Any key with a matching type + /// A new component that was attached to the same slot as the field. + /// Thrown if key doesn't belong to config, or is of wrong type + public static ModConfigurationValueSync SyncWithModConfiguration(this IField field, ModConfiguration config, ModConfigurationKey key) { + if (!config.IsKeyDefined(key)) + throw new InvalidOperationException($"Mod key ({key}) is not owned by {config.Owner.Name}'s config"); + if (key.ValueType() != typeof(T)) + throw new InvalidOperationException($"Type of mod key ({key}) does not match field type {typeof(T)}"); + + Logger.DebugInternal($"Syncing field with [{key}] from {config.Owner.Name}"); + ModConfigurationValueSync driver = field.FindNearestParent().AttachComponent>(); + driver.LoadConfigKey(config, key as ModConfigurationKey); + driver.TargetField.Target = field; + + return driver; + } +} diff --git a/ResoniteModLoader/ModLoader.cs b/ResoniteModLoader/ModLoader.cs index 179dc38..2a70c07 100644 --- a/ResoniteModLoader/ModLoader.cs +++ b/ResoniteModLoader/ModLoader.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using HarmonyLib; namespace ResoniteModLoader; @@ -16,6 +17,7 @@ public sealed class ModLoader { internal static readonly Dictionary AssemblyLookupMap = new(); // used for logging private static readonly Dictionary ModNameLookupMap = new(); // used for duplicate mod checking + private static readonly Stopwatch InitTimer = new(); // used to measure mod hooking duration /// /// Returns true if ResoniteModLoader was loaded by a headless @@ -189,9 +191,15 @@ private static void HookMod(ResoniteMod mod) { LoadProgressIndicator.SetCustom($"Starting mod [{mod.Name}/{mod.Version}]"); Logger.DebugFuncInternal(() => $"calling OnEngineInit() for [{mod.Name}/{mod.Version}]"); try { + InitTimer.Start(); mod.OnEngineInit(); } catch (Exception e) { Logger.ErrorInternal($"Mod {mod.Name} from {mod.ModAssembly?.File ?? "Unknown Assembly"} threw error from OnEngineInit():\n{e}"); } + finally { + InitTimer.Stop(); + mod.InitializationTime = InitTimer.Elapsed; + InitTimer.Reset(); + } } } diff --git a/ResoniteModLoader/ModLoaderConfiguration.cs b/ResoniteModLoader/ModLoaderConfiguration.cs index a1ba099..9291361 100644 --- a/ResoniteModLoader/ModLoaderConfiguration.cs +++ b/ResoniteModLoader/ModLoaderConfiguration.cs @@ -12,14 +12,12 @@ internal static ModLoaderConfiguration Get() { _configuration = new ModLoaderConfiguration(); Dictionary> keyActions = new() { - //{ "unsafe", (value) => _configuration.Unsafe = bool.Parse(value) }, { "debug", (value) => _configuration.Debug = bool.Parse(value) }, { "hidevisuals", (value) => _configuration.HideVisuals = bool.Parse(value) }, { "nomods", (value) => _configuration.NoMods = bool.Parse(value) }, { "advertiseversion", (value) => _configuration.AdvertiseVersion = bool.Parse(value) }, { "logconflicts", (value) => _configuration.LogConflicts = bool.Parse(value) }, - //{ "hidemodtypes", (value) => _configuration.HideModTypes = bool.Parse(value) }, - //{ "hidelatetypes", (value) => _configuration.HideLateTypes = bool.Parse(value) } + { "nodashscreen", (value) => _configuration.NoDashScreen = bool.Parse(value) }, }; // .NET's ConfigurationManager is some hot trash to the point where I'm just done with it. @@ -54,7 +52,7 @@ internal static ModLoaderConfiguration Get() { return _configuration; } - private static string GetAssemblyDirectory() { + internal static string GetAssemblyDirectory() { string codeBase = Assembly.GetExecutingAssembly().CodeBase; UriBuilder uri = new(codeBase); string path = Uri.UnescapeDataString(uri.Path); @@ -62,13 +60,11 @@ private static string GetAssemblyDirectory() { } #pragma warning disable CA1805 - //public bool Unsafe { get; private set; } = false; public bool Debug { get; private set; } = false; public bool NoMods { get; private set; } = false; public bool HideVisuals { 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; + public bool NoDashScreen { get; private set; } = false; #pragma warning restore CA1805 } diff --git a/ResoniteModLoader/Properties/AssemblyInfo.cs b/ResoniteModLoader/Properties/AssemblyInfo.cs index 043611b..58929dd 100644 --- a/ResoniteModLoader/Properties/AssemblyInfo.cs +++ b/ResoniteModLoader/Properties/AssemblyInfo.cs @@ -15,7 +15,11 @@ // Prevent FrooxEngine.Weaver from modifying this assembly, as it doesn't need anything done to it // This keeps Weaver from overwriting AssemblyVersionAttribute -[module: Description("FROOXENGINE_WEAVED")] +// [module: Description("FROOXENGINE_WEAVED")] //Mark as DataModelAssembly for the Plugin loading system to load this assembly -[assembly: DataModelAssembly(DataModelAssemblyType.Optional)] +#if DEBUG +[assembly: DataModelAssembly(DataModelAssemblyType.Core)] +#else +[assembly: DataModelAssembly(DataModelAssemblyType.UserspaceCore)] +#endif diff --git a/ResoniteModLoader/ResoniteMod.cs b/ResoniteModLoader/ResoniteMod.cs index fb1d6ee..b3e21b5 100644 --- a/ResoniteModLoader/ResoniteMod.cs +++ b/ResoniteModLoader/ResoniteMod.cs @@ -1,3 +1,5 @@ +using FrooxEngine; + namespace ResoniteModLoader; /// @@ -11,7 +13,28 @@ public abstract class ResoniteMod : ResoniteModBase { public static bool IsDebugEnabled() => Logger.IsDebugEnabled(); /// - /// Logs an object as a line in the log based on the value produced by the given function if debug logging is enabled.. + /// Logs an object as a line in the log with a stack trace based on the value produced by the given function if debug logging is enabled. + /// + /// This is more efficient than passing an or a directly, + /// as it won't be generated if debug logging is disabled. + /// + /// The function generating the object to log. + public static void TraceFunc(Func messageProducer) => Logger.TraceFuncExternal(messageProducer); + + /// + /// Logs the given object as a line in the log if debug logging is enabled. + /// + /// The object to log. + public static void Trace(object message) => Logger.TraceExternal(message); + + /// + /// Logs the given objects as lines in the log with a stack trace if debug logging is enabled. + /// + /// The objects to log. + public static void Trace(params object[] messages) => Logger.TraceListExternal(messages); + + /// + /// Logs an object as a line in the log with a stack trace based on the value produced by the given function if debug logging is enabled. /// /// This is more efficient than passing an or a directly, /// as it won't be generated if debug logging is disabled. @@ -99,4 +122,10 @@ public virtual void DefineConfiguration(ModConfigurationDefinitionBuilder builde public virtual IncompatibleConfigurationHandlingOption HandleIncompatibleConfigurationVersions(Version serializedVersion, Version definedVersion) { return IncompatibleConfigurationHandlingOption.ERROR; } + + /// + protected internal override IEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false) { + foreach (DataFeedItem item in this.GenerateModConfigurationFeed(path, groupKeys, searchPhrase, viewData, includeInternal, true)) + yield return item; + } } diff --git a/ResoniteModLoader/ResoniteModBase.cs b/ResoniteModLoader/ResoniteModBase.cs index a3f0302..3b2da9b 100644 --- a/ResoniteModLoader/ResoniteModBase.cs +++ b/ResoniteModLoader/ResoniteModBase.cs @@ -1,3 +1,7 @@ +using System.Diagnostics; +using System.Runtime.Remoting.Messaging; +using FrooxEngine; + namespace ResoniteModLoader; /// @@ -24,6 +28,8 @@ public abstract class ResoniteModBase { /// public virtual string? Link { get; } + public TimeSpan InitializationTime { get; internal set; } + /// /// A reference to the AssemblyFile that this mod was loaded from. /// The reference is set once the mod is successfully loaded, and is null before that. @@ -46,5 +52,34 @@ public abstract class ResoniteModBase { return ModConfiguration; } + /// + /// Returns whether or not this mod has a configuration, and set an out param if it does. + /// + /// The variable that is set to this mods configuration if it has one + /// true if the out param was set, false if the mod has no configuration. + public bool TryGetConfiguration(out ModConfiguration configuration) { + configuration = ModConfiguration!; + return configuration is not null; + } + + /// + /// Checks if this mod has defined a configuration. + /// + /// true if there is a config, false if there is not. + public bool HasConfiguration() => ModConfiguration is not null; + + /// + /// Define a custom configuration DataFeed for this mod. + /// + /// Starts empty at the root of the configuration category, allows sub-categories to be used. + /// Passed-through from 's Enumerate call. + /// A phrase by which configuration items should be filtered. Passed-through from 's Enumerate call + /// Passed-through from 's Enumerate call. + /// Indicates whether the user has requested that internal configuration keys are included in the returned feed. + /// DataFeedItem's to be directly returned by the calling . + protected internal abstract IEnumerable BuildConfigurationFeed(IReadOnlyList path, IReadOnlyList groupKeys, string searchPhrase, object viewData, bool includeInternal = false); + + // Why would anyone need an async config? They depend on Microsoft.Bcl.AsyncInterfaces too + internal bool FinishedLoading { get; set; } } diff --git a/ResoniteModLoader/ResoniteModLoader.csproj b/ResoniteModLoader/ResoniteModLoader.csproj index ac00e16..56bcfcf 100644 --- a/ResoniteModLoader/ResoniteModLoader.csproj +++ b/ResoniteModLoader/ResoniteModLoader.csproj @@ -5,7 +5,7 @@ false net472 512 - 10.0 + 12 enable true True @@ -26,14 +26,31 @@ + $(ResonitePath)Resonite_Data\Managed\Elements.Core.dll False + + $(ResonitePath)Resonite_Data\Managed\Elements.Quantity.dll + False + $(ResonitePath)Resonite_Data\Managed\FrooxEngine.dll False + + $(ResonitePath)Resonite_Data\Managed\FrooxEngine.Store.dll + False + + + $(ResonitePath)Resonite_Data\Managed\SkyFrost.Base.dll + False + + + $(ResonitePath)Resonite_Data\Managed\SkyFrost.Base.Models.dll + False + $(ResonitePath)Resonite_Data\Managed\Newtonsoft.Json.dll False @@ -43,7 +60,7 @@ False - R:\SteamLibrary\steamapps\common\Resonite\Resonite_Data\Managed\UnityEngine.CoreModule.dll + $(ResonitePath)Resonite_Data\Managed\UnityEngine.CoreModule.dll False @@ -52,6 +69,10 @@ + + + + diff --git a/ResoniteModLoader/Resources/DashScreenTemplate.bson b/ResoniteModLoader/Resources/DashScreenTemplate.bson new file mode 100644 index 0000000..96f2813 Binary files /dev/null and b/ResoniteModLoader/Resources/DashScreenTemplate.bson differ diff --git a/ResoniteModLoader/Util.cs b/ResoniteModLoader/Util.cs index e03b3e1..6a7af85 100644 --- a/ResoniteModLoader/Util.cs +++ b/ResoniteModLoader/Util.cs @@ -81,7 +81,8 @@ internal static bool CanBeNull(Type t) { internal static IEnumerable GetLoadableTypes(this Assembly assembly, Predicate predicate) { try { return assembly.GetTypes().Where(type => CheckType(type, predicate)); - } catch (ReflectionTypeLoadException e) { + } + catch (ReflectionTypeLoadException e) { return e.Types.Where(type => CheckType(type, predicate)); } } @@ -95,14 +96,16 @@ private static bool CheckType(Type type, Predicate predicate) { try { string _name = type.Name; - } catch (Exception e) { + } + catch (Exception e) { Logger.DebugFuncInternal(() => $"Could not read the name for a type: {e}"); return false; } try { return predicate(type); - } catch (Exception e) { + } + catch (Exception e) { Logger.DebugFuncInternal(() => $"Could not load type \"{type}\": {e}"); return false; } diff --git a/ResoniteModLoader/Utility/FeedBuilder.cs b/ResoniteModLoader/Utility/FeedBuilder.cs new file mode 100644 index 0000000..8e10f2f --- /dev/null +++ b/ResoniteModLoader/Utility/FeedBuilder.cs @@ -0,0 +1,559 @@ +using Elements.Core; +using Elements.Quantity; +using FrooxEngine; +using HarmonyLib; + +namespace ResoniteModLoader; + +/// +/// Utility class to easily generate DataFeedItem's. +/// +public static class FeedBuilder { +#pragma warning disable CS8625, CS1591, CA1715 + public static T Item(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : DataFeedItem + => Activator.CreateInstance().ChainInitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); + + // CONFLICT AB + public static DataFeedCategory Category(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + // CONFLICT BA + public static DataFeedCategory Category(string itemKey, LocaleString label, string[] subpath, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Category(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainSetOverrideSubpath(subpath); + + public static DataFeedGroup Group(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGroup Group(string itemKey, LocaleString label, IReadOnlyList subitems, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, object customEntity = null) + => Group(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGrid Grid(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGrid Grid(string itemKey, LocaleString label, IReadOnlyList subitems, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, object customEntity = null) + => Grid(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEntity Entity(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEntity Entity(string itemKey, LocaleString label, E entity, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Entity(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitEntity(entity); + + public static DataFeedAction Action(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedAction Action(string itemKey, LocaleString label, Action> setupAction, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Action(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction); + + public static DataFeedAction Action(string itemKey, LocaleString label, Action> setupAction, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Action(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction).ChainInitHighlight(setupHighlight); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, Action>> setupAction, Action> setupValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, setupValue); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, Action>> setupAction, T value, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, value); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, Action>> setupAction, Action> setupValue, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, setupValue).ChainInitHighlight(setupHighlight); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, Action>> setupAction, T value, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, value).ChainInitHighlight(setupHighlight); + + public static DataFeedSelection Selection(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, colorX color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, color color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Label(itemKey, $"{label}", path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, Action> setup, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup, format); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, T value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value, format); + public static DataFeedIndicator StringIndicator(string itemKey, LocaleString label, object value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value.ToString(), format); + + public static DataFeedToggle Toggle(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedToggle Toggle(string itemKey, LocaleString label, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Toggle(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, Func orderGetter, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => OrderedItem(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSorting(orderGetter); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, long order, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => OrderedItem(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSorting(order); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, Action> setup, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, Action> setup, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, T>(formatting); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, Action> setup, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, E>(setupFormatting); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, Action> setup, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, E>(formatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, T min, T max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, Action> value, T min, T max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, Action> value, Action> min, Action> max, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitUnitConfiguration(defaultConfig, imperialConfig); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, Action> value, T min, T max, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitUnitConfiguration(defaultConfig, imperialConfig); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, Action> setupFormatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, Action> min, Action> max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, Action> setupFormatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, Action> value, T min, T max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); + + // With description + + public static T Item(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : DataFeedItem + => Activator.CreateInstance().ChainInitBase(itemKey, path, groupingParameters, label, description, icon, setupVisible, setupEnabled, subitems, customEntity); + + // CONFLICT CD + public static DataFeedCategory Category(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + // CONFLICT DC + public static DataFeedCategory Category(string itemKey, LocaleString label, LocaleString description, string[] subpath, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Category(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainSetOverrideSubpath(subpath); + + public static DataFeedGroup Group(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGroup Group(string itemKey, LocaleString label, LocaleString description, IReadOnlyList subitems, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, object customEntity = null) + => Group(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGrid Grid(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedGrid Grid(string itemKey, LocaleString label, LocaleString description, IReadOnlyList subitems, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, object customEntity = null) + => Grid(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEntity Entity(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEntity Entity(string itemKey, LocaleString label, LocaleString description, E entity, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Entity(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitEntity(entity); + + public static DataFeedAction Action(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedAction Action(string itemKey, LocaleString label, LocaleString description, Action> setupAction, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Action(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction); + + public static DataFeedAction Action(string itemKey, LocaleString label, LocaleString description, Action> setupAction, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Action(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction).ChainInitHighlight(setupHighlight); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, Action>> setupAction, Action> setupValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, setupValue); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, Action>> setupAction, T value, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, value); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, Action>> setupAction, Action> setupValue, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, setupValue).ChainInitHighlight(setupHighlight); + + public static DataFeedValueAction ValueAction(string itemKey, LocaleString label, LocaleString description, Action>> setupAction, T value, Action> setupHighlight, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueAction(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitAction(setupAction, value).ChainInitHighlight(setupHighlight); + + public static DataFeedSelection Selection(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleString description, colorX color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Label(itemKey, $"{label}", description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedLabel Label(string itemKey, LocaleString label, LocaleString description, color color, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Label(itemKey, $"{label}", description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, LocaleString description, Action> setup, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup, format); + + public static DataFeedIndicator Indicator(string itemKey, LocaleString label, LocaleString description, T value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value, format); + public static DataFeedIndicator StringIndicator(string itemKey, LocaleString label, LocaleString description, object value, string format = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Indicator(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue((field) => field.Value = value.ToString(), format); + + public static DataFeedToggle Toggle(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedToggle Toggle(string itemKey, LocaleString label, LocaleString description, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Toggle(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, LocaleString description, Func orderGetter, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => OrderedItem(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSorting(orderGetter); + + public static DataFeedOrderedItem OrderedItem(string itemKey, LocaleString label, LocaleString description, long order, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where T : IComparable + => OrderedItem(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSorting(order); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, LocaleString description, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, LocaleString description, Action> setup, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedValueField ValueField(string itemKey, LocaleString label, LocaleString description, Action> setup, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, T>(formatting); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, LocaleString description, Action> setup, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, LocaleString description, Action> setup, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, E>(setupFormatting); + + public static DataFeedEnum Enum(string itemKey, LocaleString label, LocaleString description, Action> setup, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where E : Enum + => Enum(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetupValue(setup).ChainInitFormatting, E>(formatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedClampedValueField ClampedValueField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => ClampedValueField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitUnitConfiguration(defaultConfig, imperialConfig); + + public static DataFeedQuantityField QuantityField(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where Q : unmanaged, IQuantity + => QuantityField(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitUnitConfiguration(defaultConfig, imperialConfig); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Item>(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, Action> setupFormatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, string formatting, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, Action> setupFormatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, Action> min, Action> max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, Action> setupFormatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(setupFormatting); + + public static DataFeedSlider Slider(string itemKey, LocaleString label, LocaleString description, Action> value, T min, T max, string formatting, Action> setupReferenceValue, IReadOnlyList path = null, IReadOnlyList groupingParameters = null, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) + => Slider(itemKey, label, description, path, groupingParameters, icon, setupVisible, setupEnabled, subitems, customEntity).ChainInitSetup(value, min, max).ChainInitSlider(setupReferenceValue).ChainInitFormatting, T>(formatting); +#pragma warning restore CS8625, CS1591, CA1715 +} + +/// +/// Extends all DataFeedItem's "Init" methods so they can be called in a chain (methods return original item). +/// +public static class FeedBuilderExtensions { +#pragma warning disable CS8625, CA1715 + /// Mapped to InitBase + public static I ChainInitBase(this I item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where I : DataFeedItem { + item.InitBase(itemKey, path, groupingParameters, label, icon, setupVisible, setupEnabled, subitems, customEntity); + return item; + } + + /// Mapped to InitBase + public static I ChainInitBase(this I item, string itemKey, IReadOnlyList path, IReadOnlyList groupingParameters, LocaleString label, LocaleString description, Uri icon = null, Action> setupVisible = null, Action> setupEnabled = null, IReadOnlyList subitems = null, object customEntity = null) where I : DataFeedItem { + item.InitBase(itemKey, path, groupingParameters, label, description, icon, setupVisible, setupEnabled, subitems, customEntity); + return item; + } + + /// Mapped to InitVisible + public static I ChainInitVisible(this I item, Action> setupVisible) where I : DataFeedItem { + item.InitVisible(setupVisible); + return item; + } + + /// Mapped to InitEnabled + public static I ChainInitEnabled(this I item, Action> setupEnabled) where I : DataFeedItem { + item.InitEnabled(setupEnabled); + return item; + } + + /// Mapped to InitDescription + public static I ChainInitDescription(this I item, LocaleString description) where I : DataFeedItem { + item.InitDescription(description); + return item; + } + + /// Mapped to InitSorting + public static I ChainInitSorting(this I item, long order) where I : DataFeedItem { + item.InitSorting(order); + return item; + } + + /// Mapped to InitSorting + public static I ChainInitSorting(this I item, Func orderGetter) where I : DataFeedItem { + item.InitSorting(orderGetter); + return item; + } + + /// Mapped to SetOverrideSubpath + public static DataFeedCategory ChainSetOverrideSubpath(this DataFeedCategory item, params string[] subpath) { + item.SetOverrideSubpath(subpath); + return item; + } + + /// Mapped to InitEntity + public static DataFeedEntity ChainInitEntity(this DataFeedEntity item, E entity) { + item.InitEntity(entity); + return item; + } + + /// Mapped to InitSetupValue + public static I ChainInitSetupValue(this I item, Action> setup) where I : DataFeedValueElement { + item.InitSetupValue(setup); + return item; + } + + /// Mapped to InitFormatting + public static I ChainInitFormatting(this I item, Action> setupFormatting) where I : DataFeedValueElement { + item.InitFormatting(setupFormatting); + return item; + } + + /// Mapped to InitFormatting + public static I ChainInitFormatting(this I item, string formatting) where I : DataFeedValueElement { + item.InitFormatting(formatting); + return item; + } + + /// Mapped to InitSetup + public static DataFeedOrderedItem ChainInitSetup(this DataFeedOrderedItem item, Action> orderValue, Action> setupIsFirst, Action> setupIsLast, Action> setupMoveUp, Action> setupMoveDown, Action> setupMakeFirst, Action> setupMakeLast, LocaleString moveUpLabel = default, LocaleString moveDownLabel = default, LocaleString makeFirstLabel = default, LocaleString makeLastLabel = default) where T : IComparable { + item.InitSetup(orderValue, setupIsFirst, setupIsLast, setupMoveUp, setupMoveDown, setupMakeFirst, setupMakeLast, moveUpLabel, moveDownLabel, makeFirstLabel, makeLastLabel); + return item; + } + + /// Mapped to InitSetup + public static I ChainInitSetup(this I item, Action> value, Action> min, Action> max) where I : DataFeedClampedValueField { + item.InitSetup(value, min, max); + return item; + } + /// Mapped to InitSetup + public static I ChainInitSetup(this I item, Action> value, T min, T max) where I : DataFeedClampedValueField { + item.InitSetup(value, min, max); + return item; + } + + /// Mapped to InitUnitConfiguration + public static DataFeedQuantityField ChainInitUnitConfiguration(this DataFeedQuantityField item, UnitConfiguration defaultConfig, UnitConfiguration imperialConfig = null) where Q : unmanaged, IQuantity { + item.InitUnitConfiguration(defaultConfig, imperialConfig); + return item; + } + + /// Mapped to InitSlider + public static DataFeedSlider ChainInitSlider(this DataFeedSlider item, Action> setupReferenceValue) { + item.InitSlider(setupReferenceValue); + return item; + } + + /// Mapped to InitAction + public static DataFeedAction ChainInitAction(this DataFeedAction item, Action> setupAction) { + item.InitAction(setupAction); + return item; + } + + /// Mapped to InitHighlight + public static DataFeedAction ChainInitHighlight(this DataFeedAction item, Action> setupHighlight) { + item.InitHighlight(setupHighlight); + return item; + } + + /// Mapped to InitAction + public static DataFeedValueAction ChainInitAction(this DataFeedValueAction item, Action>> setupAction, Action> setupValue) { + item.InitAction(setupAction, setupValue); + return item; + } + + /// Mapped to InitAction + public static DataFeedValueAction ChainInitAction(this DataFeedValueAction item, Action>> setupAction, T value) { + item.InitAction(setupAction, value); + return item; + } + + /// Mapped to InitHighlight + public static DataFeedValueAction ChainInitHighlight(this DataFeedValueAction item, Action> setupHighlight) { + item.InitHighlight(setupHighlight); + return item; + } + + /// Mapped to InitSetupValue + public static DataFeedIndicator ChainInitSetupValue(this DataFeedIndicator item, Action> setup, string format = null) { + item.InitSetupValue(setup, format); + return item; + } + + private static PropertyInfo SubItemsSetter = typeof(DataFeedItem).GetProperty(nameof(DataFeedItem.SubItems)); + + public static I AddSubitems(this I item, params DataFeedItem[] subitems) where I : DataFeedItem { + if (item.SubItems is null) + SubItemsSetter.SetValue(item, subitems.ToList().AsReadOnly(), null); + else + SubItemsSetter.SetValue(item, item.SubItems.Concat(subitems).ToList().AsReadOnly(), null); + return item; + } + + public static I ReplaceSubitems(this I item, params DataFeedItem[] subitems) where I : DataFeedItem { + SubItemsSetter.SetValue(item, subitems.ToList().AsReadOnly(), null); + return item; + } + + public static I ClearSubitems(this I item) where I : DataFeedItem { + SubItemsSetter.SetValue(item, null, null); + return item; + } +#pragma warning restore CS8625, CA1715 +} diff --git a/doc/datafeed.md b/doc/datafeed.md new file mode 100644 index 0000000..143f37d --- /dev/null +++ b/doc/datafeed.md @@ -0,0 +1,9 @@ +# Configuration feeds & you + +FeedBuilder +Logger +ModConfigurationDataFeed +ModConfigurationFeedBuilder +ModConfigurationValueSync +ResoniteMod.BuildConfigurationFeed +AutoRegisterConfigKeyAttribute.Group