diff --git a/Osu.Patcher.Hook/Patches/CustomStrings/AddEnumValueNames.cs b/Osu.Patcher.Hook/Patches/CustomStrings/AddEnumValueNames.cs new file mode 100644 index 0000000..2146dc1 --- /dev/null +++ b/Osu.Patcher.Hook/Patches/CustomStrings/AddEnumValueNames.cs @@ -0,0 +1,39 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; +using JetBrains.Annotations; +using Osu.Stubs.Helpers; + +namespace Osu.Patcher.Hook.Patches.CustomStrings; + +/// +/// Patch Enum::ToString() to add additional enum values that can be stringified. +/// +[OsuPatch] +[HarmonyPatch] +[UsedImplicitly] +internal static class AddEnumValueNames +{ + [UsedImplicitly] + [HarmonyTargetMethod] + private static MethodBase Target() => typeof(Enum).Method(nameof(Enum.GetName)); + + [HarmonyPrefix] + [UsedImplicitly] + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static bool Before( + ref string __result, + [HarmonyArgument(0)] Type enumType, + [HarmonyArgument(1)] object? value) + { + if (value == null) return true; + + var valueIdx = Convert.ToUInt32(value); + + if (enumType == OsuString.Class.Reference) + return !CustomStrings.OsuStringNames.TryGetValue(valueIdx, out __result); + + return true; + } +} \ No newline at end of file diff --git a/Osu.Patcher.Hook/Patches/CustomStrings/AddLocalizedStrings.cs b/Osu.Patcher.Hook/Patches/CustomStrings/AddLocalizedStrings.cs new file mode 100644 index 0000000..2349211 --- /dev/null +++ b/Osu.Patcher.Hook/Patches/CustomStrings/AddLocalizedStrings.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using HarmonyLib; +using JetBrains.Annotations; +using Osu.Stubs.Helpers; + +namespace Osu.Patcher.Hook.Patches.CustomStrings; + +/// +/// Adds the new hardcoded OsuStrings into the LocalisationManager. +/// +[OsuPatch] +[HarmonyPatch] +[UsedImplicitly] +internal static class AddLocalizedStrings +{ + // Perhaps add localization support in the future? + + [UsedImplicitly] + [HarmonyTargetMethod] + private static MethodBase Target() => LocalisationManager.GetString.Reference; + + [HarmonyPrefix] + [UsedImplicitly] + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static bool Before(ref string __result, [HarmonyArgument(0)] int osuString) => + !CustomStrings.OsuStrings.TryGetValue((uint)osuString, out __result); +} \ No newline at end of file diff --git a/Osu.Patcher.Hook/Patches/CustomStrings/CustomStrings.cs b/Osu.Patcher.Hook/Patches/CustomStrings/CustomStrings.cs new file mode 100644 index 0000000..463cce7 --- /dev/null +++ b/Osu.Patcher.Hook/Patches/CustomStrings/CustomStrings.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Osu.Utils.Extensions; + +namespace Osu.Patcher.Hook.Patches.CustomStrings; + +/// +/// Store custom string values to be injected into the game later. +/// +internal static class CustomStrings +{ + /// + /// New OsuString value index -> new enum value name + /// + internal static readonly Dictionary OsuStringNames = new(); + + /// + /// New OsuString value index -> the localized value. + /// + internal static readonly Dictionary OsuStrings = new(); + + private static uint _lastOsuString = 0x00010000; + + /// + /// Add a new value to be stringified if it is not already added. + /// + /// The name of the enum value. (So ToString() works properly) + /// The hardcoded localized string value (no multi-language support) + /// The value to use to reference this entry as enums are valuetypes. + internal static int AddOsuString(string name, string value) + { + if (OsuStringNames.TryGetKey(name, out var key)) + return (int)key; + + var idx = ++_lastOsuString; + OsuStringNames.Add(idx, name); + OsuStrings.Add(idx, value); + return (int)idx; + } +} \ No newline at end of file diff --git a/Osu.Stubs/Helpers/LocalisationManager.cs b/Osu.Stubs/Helpers/LocalisationManager.cs new file mode 100644 index 0000000..1b8fb8b --- /dev/null +++ b/Osu.Stubs/Helpers/LocalisationManager.cs @@ -0,0 +1,35 @@ +using JetBrains.Annotations; +using Osu.Utils.Lazy; +using static System.Reflection.Emit.OpCodes; + +namespace Osu.Stubs.Helpers; + +/// +/// Original: osu_common.Helpers.LocalisationManager +/// b20240123: +/// +[PublicAPI] +public class LocalisationManager +{ + /// + /// Original: GetString(OsuString stringType) + /// b20240123: + /// + [Stub] + public static readonly LazyMethod GetString = LazyMethod.ByPartialSignature( + "osu_common.Helpers.LocalisationManager::GetString(OsuString)", + [ + Ldsfld, + Ldarg_0, + Callvirt, + Stloc_0, + Leave_S, + Pop, + Ldsfld, + Stloc_0, + Leave_S, + Ldloc_0, + Ret, + ] + ); +} \ No newline at end of file diff --git a/Osu.Stubs/Helpers/OsuString.cs b/Osu.Stubs/Helpers/OsuString.cs new file mode 100644 index 0000000..26ec098 --- /dev/null +++ b/Osu.Stubs/Helpers/OsuString.cs @@ -0,0 +1,15 @@ +using JetBrains.Annotations; +using Osu.Utils.Lazy; + +namespace Osu.Stubs.Helpers; + +[PublicAPI] +public class OsuString +{ + /// + /// Original: osu_common.Helpers.OsuString + /// b20240123: + /// + [Stub] + public static readonly LazyType Class = LazyType.ByName("osu_common.Helpers.OsuString"); +} \ No newline at end of file diff --git a/Osu.Utils/Extensions/Extensions.cs b/Osu.Utils/Extensions/Extensions.cs index 12626eb..c892e1d 100644 --- a/Osu.Utils/Extensions/Extensions.cs +++ b/Osu.Utils/Extensions/Extensions.cs @@ -5,6 +5,28 @@ namespace Osu.Utils.Extensions; public static class Extensions { + /// + /// Find the key for a value. This is inefficient. + /// + /// The dictionary to search in. + /// Value to search for. + /// Out value for the found key if returning true, otherwise default. + /// True if found. + public static bool TryGetKey(this Dictionary dict, TValue value, out TKey key) + { + foreach (var pair in dict) + { + if (!EqualityComparer.Default.Equals(pair.Value, value)) + continue; + + key = pair.Key; + return true; + } + + key = default!; + return false; + } + /// /// Filters out null values in this sequence. ///