Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial ModConfigurationDataFeed implementation #15

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
00289f4
Initial data feed and dash screen test
djsime1 Jul 15, 2024
746c493
Fix compiler async issues
djsime1 Jul 15, 2024
ec8e7d9
Revert "remove Unsafe/HideModTypes/HideLateTypes modloader config opt…
djsime1 Jul 15, 2024
33ef2ab
It sort of works?
djsime1 Jul 16, 2024
37f6943
It works better
djsime1 Jul 17, 2024
87d1859
Prepare log querying and refactor with new FeedBuilder utility class
djsime1 Jul 19, 2024
f8a336b
Add value fields and descriptions to FeedBuilder
djsime1 Jul 19, 2024
2d250bc
A bunch of progress
djsime1 Jul 21, 2024
edae026
Generate enum fields separately
djsime1 Jul 21, 2024
d5ad61a
Logger revamp and ConfigurationFeedBuilder grouping/subcategories
djsime1 Jul 22, 2024
e1bc478
Pro tip: don't try to use an array as a dictionary key!
djsime1 Jul 23, 2024
82a8887
Remove subcategories from ModConfigurationFeedBuilder
djsime1 Jul 23, 2024
36cd4d2
Refactor logger for exception handling
djsime1 Aug 5, 2024
b0f7149
Implement initialization time recording, Display mod logs/exceptions
djsime1 Aug 5, 2024
390f682
ModConfigurationValueSync finally works, praise the sun
djsime1 Aug 5, 2024
bf6df61
Reapply "remove Unsafe/HideModTypes/HideLateTypes modloader config op…
djsime1 Aug 6, 2024
5261a4b
Properly exclude RML's assembly from getting hidden so data model ite…
djsime1 Aug 6, 2024
df7a3a9
Document all the things
djsime1 Aug 13, 2024
36ef92e
ConfigurationFeedBuilder improvements
djsime1 Aug 20, 2024
ee4b0ce
I havent touched this repo in months but I left git dirty so here's w…
djsime1 Nov 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 38 additions & 39 deletions ResoniteModLoader/AssemblyHider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,25 @@ internal static class AssemblyHider {
/// <param name="harmony">Our RML harmony instance</param>
/// <param name="initialAssemblies">Assemblies that were loaded when RML first started</param>
internal static void PatchResonite(Harmony harmony, HashSet<Assembly> 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<Type>());
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<Type>());
MethodInfo getAssembliesPatch = AccessTools.DeclaredMethod(typeof(AssemblyHider), nameof(GetAssembliesPostfix));
harmony.Patch(getAssembliesTarget, postfix: new HarmonyMethod(getAssembliesPatch));
}

private static HashSet<Assembly> GetResoniteAssemblies(HashSet<Assembly> initialAssemblies) {
Expand All @@ -88,6 +86,9 @@ private static HashSet<Assembly> GetModAssemblies(HashSet<Assembly> 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;
}
Expand All @@ -104,26 +105,24 @@ private static HashSet<Assembly> GetModAssemblies(HashSet<Assembly> 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;
}
}

Expand Down
23 changes: 22 additions & 1 deletion ResoniteModLoader/Attributes/AutoRegisterConfigKeyAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,25 @@ namespace ResoniteModLoader;
/// deriving from <see cref="ResoniteMod"/> to be automatically included in that mod's configuration.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public sealed class AutoRegisterConfigKeyAttribute : Attribute { }
public sealed class AutoRegisterConfigKeyAttribute : Attribute {

/// <summary>
/// Defines a group that this configuration key belongs to, used by default configuration feed builder.
/// </summary>
public string? Group => _group;

private readonly string? _group;

/// <summary>
/// Flag this field to be automatically registered as a configuration key for this mod that is not grouped with any other keys.
/// </summary>
public AutoRegisterConfigKeyAttribute() { }

/// <summary>
/// Flag this field to be automatically registered as a configuration key for this mod that is part of a group.
/// </summary>
/// <param name="group">The name of the group this configuration key belongs to.</param>
public AutoRegisterConfigKeyAttribute(string group) {
_group = group;
}
}
82 changes: 82 additions & 0 deletions ResoniteModLoader/DashScreenInjector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using FrooxEngine;
using FrooxEngine.UIX;
using HarmonyLib;

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;
}

Logger.DebugInternal("Injecting dash screen");

RadiantDash dash = __instance.Slot.GetComponentInParents<RadiantDash>();
InjectedScreen = dash.AttachScreen("Mods", RadiantUI_Constants.Neutrals.LIGHT, OfficialAssets.Graphics.Icons.Dash.Tools);

InjectedScreen.Slot.OrderOffset = 128;
InjectedScreen.Slot.PersistentSelf = false;

SingleFeedView view = InjectedScreen.ScreenRoot.AttachComponent<SingleFeedView>();
ModConfigurationDataFeed feed = InjectedScreen.ScreenRoot.AttachComponent<ModConfigurationDataFeed>();

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>();
rootCategoryView.Slot.GetComponentInChildren<BreadcrumbManager>().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<BreadcrumbInterface>().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<DataFeedItemMapper>();
Canvas tempCanvas = templates.AttachComponent<Canvas>(); // 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<VerticalLayout>(); // just for debugging
}
else {
Logger.ErrorInternal("Failed to load SettingsItemMappers for dash screen, aborting and cleaning up.");
InjectedScreen.Slot.Destroy();
return;
}

InjectedScreen.ScreenCanvas.Slot.AttachComponent<Image>().Tint.Value = UserspaceRadiantDash.DEFAULT_BACKGROUND;
view.Feed.Target = feed;
view.SetCategoryPath(["ResoniteModLoader"]);

Logger.DebugInternal("Dash screen should be injected!");
}
}
2 changes: 2 additions & 0 deletions ResoniteModLoader/DebugInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())}");
Expand Down
7 changes: 7 additions & 0 deletions ResoniteModLoader/ExecutionHook.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics;
using FrooxEngine;

namespace ResoniteModLoader;
Expand All @@ -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<string, AssemblyTypeRegistry>)typeof(GlobalTypeRegistry).GetField("_byName", flags).GetValue(null);

Expand All @@ -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}");
}
Expand Down
1 change: 1 addition & 0 deletions ResoniteModLoader/HarmonyWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ internal static void LoadModsAndHideModAssemblies(HashSet<Assembly> initialAssem
ModLoader.LoadMods();
ModConfiguration.RegisterShutdownHook(harmony);
AssemblyHider.PatchResonite(harmony, initialAssemblies);
DashScreenInjector.PatchScreenManager(harmony);
}
}
Loading