BUTR/Bannerlord.BLSE

View on GitHub
src/Bannerlord.LauncherEx/ResourceManagers/BrushFactoryManager.cs

Summary

Maintainability
A
2 hrs
Test Coverage
using HarmonyLib;
using HarmonyLib.BUTR.Extensions;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;

using TaleWorlds.GauntletUI;

namespace Bannerlord.LauncherEx.ResourceManagers;

internal static class BrushFactoryManager
{
    private static readonly Dictionary<string, Brush> CustomBrushes = new();
    private static readonly List<Func<IEnumerable<Brush>>> DeferredInitialization = new();

    private delegate Brush LoadBrushFromDelegate(BrushFactory instance, XmlNode brushNode);
    private static readonly LoadBrushFromDelegate? LoadBrushFrom =
        AccessTools2.GetDelegate<LoadBrushFromDelegate>(typeof(BrushFactory), "LoadBrushFrom");

    private static Harmony? _harmony;
    private static WeakReference<BrushFactory?> BrushFactoryReference { get; } = new(null);
    public static void SetBrushFactory(BrushFactory brushFactory)
    {
        BrushFactoryReference.SetTarget(brushFactory);

        foreach (var brush in DeferredInitialization.SelectMany(func => func()))
        {
            CustomBrushes[brush.Name] = brush;
        }
    }

    public static IEnumerable<Brush> Create(XmlDocument xmlDocument)
    {
        if (!BrushFactoryReference.TryGetTarget(out var brushFactory) || brushFactory is null)
            yield break;

        foreach (var brushNode in xmlDocument.SelectSingleNode("Brushes")!.ChildNodes.OfType<XmlNode>())
        {
            var brush = LoadBrushFrom?.Invoke(brushFactory, brushNode);
            if (brush is not null)
            {
                yield return brush;
            }
        }
    }
    public static void Register(Func<IEnumerable<Brush>> func) => DeferredInitialization.Add(func);
    public static void CreateAndRegister(XmlDocument xmlDocument) => Register(() => Create(xmlDocument));

    public static void Clear()
    {
        CustomBrushes.Clear();
        DeferredInitialization.Clear();
        BrushFactoryReference.SetTarget(null);
    }

    internal static bool Enable(Harmony harmony)
    {
        _harmony = harmony;

        var res1 = harmony.TryPatch(
            AccessTools2.PropertyGetter(typeof(BrushFactory), "Brushes"),
            postfix: AccessTools2.DeclaredMethod(typeof(BrushFactoryManager), nameof(BrushesPostfix)));
        if (!res1) return false;

        var res2 = harmony.TryPatch(
            AccessTools2.DeclaredMethod(typeof(BrushFactory), "CheckForUpdates"),
            prefix: AccessTools2.DeclaredMethod(typeof(BrushFactoryManager), nameof(CheckForUpdatesPrefix)));
        if (!res2) return false;

        var res3 = harmony.TryPatch(
            AccessTools2.DeclaredMethod(typeof(BrushFactory), "GetBrush"),
            AccessTools2.DeclaredMethod(typeof(BrushFactoryManager), nameof(GetBrushPrefix)));
        if (!res3) return false;

        var res4 = harmony.TryPatch(
            AccessTools2.Method(typeof(BrushFactory), "LoadBrushes"),
            AccessTools2.DeclaredMethod(typeof(BrushFactoryManager), nameof(LoadBrushesPostfix)));
        if (!res4) return false;

        return true;
    }

    private static void BrushesPostfix(ref IEnumerable<Brush> __result)
    {
        __result = __result.Concat(CustomBrushes.Values);
    }

    // We need to disable BrushFactory's hot reload
    private static bool CheckForUpdatesPrefix()
    {
        return false;
    }

    private static bool GetBrushPrefix(string name, Dictionary<string, Brush> ____brushes, ref Brush __result)
    {
        if (____brushes.ContainsKey(name) || !CustomBrushes.ContainsKey(name))
            return true;

        if (CustomBrushes[name] is { } brush)
        {
            __result = brush;
            return false;
        }

        return true;
    }


    private static void LoadBrushesPostfix(ref BrushFactory __instance)
    {
        SetBrushFactory(__instance);

        _harmony?.Unpatch(
            AccessTools2.Method(typeof(BrushFactory), "LoadBrushes"),
            AccessTools2.DeclaredMethod(typeof(BrushFactoryManager), nameof(LoadBrushesPostfix)));
    }

    private static IEnumerable<CodeInstruction> BlankTranspiler(IEnumerable<CodeInstruction> instructions) => instructions;
}