diff --git a/Directory.Build.props b/Directory.Build.props
index 3052ee6..3ecaf53 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -31,7 +31,7 @@
-
+
$(MSBuildThisFileDirectory)\stylecop.ruleset
diff --git a/Reactor.Example/ExampleLocalizationProvider.cs b/Reactor.Example/ExampleLocalizationProvider.cs
new file mode 100644
index 0000000..90c8418
--- /dev/null
+++ b/Reactor.Example/ExampleLocalizationProvider.cs
@@ -0,0 +1,26 @@
+using Reactor.Localization;
+
+namespace Reactor.Example;
+
+public class ExampleLocalizationProvider : LocalizationProvider
+{
+ public override bool TryGetText(StringNames stringName, out string? result)
+ {
+ if (stringName == (StringNames) 1337)
+ {
+ switch (CurrentLanguage)
+ {
+ case SupportedLangs.English:
+ result = "Cringe English";
+ return true;
+
+ default:
+ result = "Based " + CurrentLanguage;
+ return true;
+ }
+ }
+
+ result = null;
+ return false;
+ }
+}
diff --git a/Reactor.Example/ExamplePlugin.cs b/Reactor.Example/ExamplePlugin.cs
index fc1922d..4a768e8 100644
--- a/Reactor.Example/ExamplePlugin.cs
+++ b/Reactor.Example/ExamplePlugin.cs
@@ -2,6 +2,8 @@
using BepInEx;
using BepInEx.Unity.IL2CPP;
using Il2CppInterop.Runtime.Attributes;
+using Reactor.Localization;
+using Reactor.Localization.Utilities;
using Reactor.Networking;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
@@ -19,9 +21,14 @@ namespace Reactor.Example;
[ReactorModFlags(ModFlags.RequireOnAllClients)]
public partial class ExamplePlugin : BasePlugin
{
+ private static StringNames _helloStringName;
+
public override void Load()
{
this.AddComponent();
+
+ _helloStringName = CustomStringName.CreateAndRegister("Hello!");
+ LocalizationManager.Register(new ExampleLocalizationProvider());
}
[RegisterInIl2Cpp]
@@ -34,19 +41,29 @@ public ExampleComponent(IntPtr ptr) : base(ptr)
{
TestWindow = new DragWindow(new Rect(60, 20, 0, 0), "Example", () =>
{
+ if (GUILayout.Button("Log CustomStringName"))
+ {
+ Logger.Info(TranslationController.Instance.GetString(_helloStringName));
+ }
+
+ if (GUILayout.Button("Log localized string"))
+ {
+ Logger.Info(TranslationController.Instance.GetString((StringNames) 1337));
+ }
+
if (AmongUsClient.Instance && PlayerControl.LocalPlayer)
{
if (GUILayout.Button("Send ExampleRpc"))
{
- var name = PlayerControl.LocalPlayer.Data.PlayerName;
- Rpc.Instance.Send(new ExampleRpc.Data($"Send: from {name}"), ackCallback: () =>
+ var playerName = PlayerControl.LocalPlayer.Data.PlayerName;
+ Rpc.Instance.Send(new ExampleRpc.Data($"Send: from {playerName}"), ackCallback: () =>
{
Logger.Info("Got an acknowledgement for example rpc");
});
if (!AmongUsClient.Instance.AmHost)
{
- Rpc.Instance.SendTo(AmongUsClient.Instance.HostId, new ExampleRpc.Data($"SendTo: from {name} to host"));
+ Rpc.Instance.SendTo(AmongUsClient.Instance.HostId, new ExampleRpc.Data($"SendTo: from {playerName} to host"));
}
}
diff --git a/Reactor.sln b/Reactor.sln
index 11fdc70..ba2a0bc 100644
--- a/Reactor.sln
+++ b/Reactor.sln
@@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Files", "Solution
AmongUs.props = AmongUs.props
Directory.Build.props = Directory.Build.props
nuget.config = nuget.config
+ stylecop.json = stylecop.json
+ stylecop.ruleset = stylecop.ruleset
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Reactor.Networking.Shared", "Reactor.Networking.Shared\Reactor.Networking.Shared.csproj", "{1E68CA20-22EB-4E57-B525-81970BE6363D}"
diff --git a/Reactor/Localization/CustomStringName.cs b/Reactor/Localization/CustomStringName.cs
deleted file mode 100644
index 7034dd4..0000000
--- a/Reactor/Localization/CustomStringName.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using HarmonyLib;
-using Il2CppInterop.Runtime.InteropTypes.Arrays;
-
-namespace Reactor.Localization;
-
-///
-/// Provides a way to use custom static StringNames.
-///
-public class CustomStringName
-{
- private static int _lastId = -1;
-
- private static readonly List _list = new();
-
- ///
- /// Gets a list of all s.
- ///
- public static IReadOnlyList List => _list.AsReadOnly();
-
- ///
- /// Registers a with specified .
- ///
- /// The value.
- /// A .
- public static CustomStringName Register(string value)
- {
- var customStringName = new CustomStringName(_lastId--, value);
- _list.Add(customStringName);
-
- return customStringName;
- }
-
- ///
- /// Gets the id.
- ///
- public int Id { get; }
-
- ///
- /// Gets the value.
- ///
- public string Value { get; }
-
- private CustomStringName(int id, string value)
- {
- Id = id;
- Value = value;
- }
-
- ///
- /// Defines an implicit conversion of a to a .
- ///
- /// The .
- /// A .
- public static implicit operator StringNames(CustomStringName name) => (StringNames) name.Id;
-
- ///
- /// Defines an explicit conversion of a to a .
- ///
- /// The .
- /// A .
- public static explicit operator CustomStringName?(StringNames name) => List.SingleOrDefault(x => x.Id == (int) name);
-
- [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), typeof(StringNames), typeof(Il2CppReferenceArray))]
- private static class GetStringPatch
- {
- public static bool Prefix(StringNames id, Il2CppReferenceArray parts, ref string __result)
- {
- var customStringName = (CustomStringName?) id;
-
- if (customStringName != null)
- {
- __result = string.Format(CultureInfo.InvariantCulture, customStringName.Value, parts);
- return false;
- }
-
- return true;
- }
- }
-}
diff --git a/Reactor/Localization/Extensions/SupportedLangsExtensions.cs b/Reactor/Localization/Extensions/SupportedLangsExtensions.cs
new file mode 100644
index 0000000..03c26ef
--- /dev/null
+++ b/Reactor/Localization/Extensions/SupportedLangsExtensions.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Globalization;
+
+namespace Reactor.Localization.Extensions;
+
+///
+/// Provides extension methods for .
+///
+public static class SupportedLangsExtensions
+{
+ ///
+ /// Gets a from the specified .
+ ///
+ /// The .
+ /// a .
+ public static CultureInfo ToCultureInfo(this SupportedLangs language)
+ {
+ return language switch
+ {
+ SupportedLangs.English => CultureInfo.GetCultureInfo("en"),
+ SupportedLangs.Latam => CultureInfo.GetCultureInfo("es"),
+ SupportedLangs.Brazilian => CultureInfo.GetCultureInfo("pt-BR"),
+ SupportedLangs.Portuguese => CultureInfo.GetCultureInfo("pt"),
+ SupportedLangs.Korean => CultureInfo.GetCultureInfo("ko"),
+ SupportedLangs.Russian => CultureInfo.GetCultureInfo("ru"),
+ SupportedLangs.Dutch => CultureInfo.GetCultureInfo("nl"),
+ SupportedLangs.Filipino => CultureInfo.GetCultureInfo("fil"),
+ SupportedLangs.French => CultureInfo.GetCultureInfo("fr"),
+ SupportedLangs.German => CultureInfo.GetCultureInfo("de"),
+ SupportedLangs.Italian => CultureInfo.GetCultureInfo("it"),
+ SupportedLangs.Japanese => CultureInfo.GetCultureInfo("ja"),
+ SupportedLangs.Spanish => CultureInfo.GetCultureInfo("es"),
+ SupportedLangs.SChinese => CultureInfo.GetCultureInfo("zh-Hans"),
+ SupportedLangs.TChinese => CultureInfo.GetCultureInfo("zh-Hant"),
+ SupportedLangs.Irish => CultureInfo.GetCultureInfo("ga"),
+ _ => throw new ArgumentOutOfRangeException(nameof(language), language, null),
+ };
+ }
+}
diff --git a/Reactor/Localization/LocalizationManager.cs b/Reactor/Localization/LocalizationManager.cs
new file mode 100644
index 0000000..6f12b09
--- /dev/null
+++ b/Reactor/Localization/LocalizationManager.cs
@@ -0,0 +1,111 @@
+using System.Collections.Generic;
+using Il2CppInterop.Runtime.InteropTypes.Arrays;
+
+namespace Reactor.Localization;
+
+///
+/// Handles custom localization.
+///
+public static class LocalizationManager
+{
+ private static readonly List _providers = new();
+
+ ///
+ /// Gets registered s.
+ ///
+ public static IReadOnlyList Providers { get; } = _providers.AsReadOnly();
+
+ ///
+ /// Registers a new to be used for obtaining translations.
+ ///
+ /// A instance.
+ public static void Register(LocalizationProvider provider)
+ {
+ if (!_providers.Contains(provider))
+ {
+ _providers.Add(provider);
+
+ if (TranslationController.InstanceExists)
+ {
+ provider.SetLanguage(TranslationController.Instance.currentLanguage.languageID);
+ }
+
+ _providers.Sort((a, b) => b.Priority - a.Priority);
+ }
+ }
+
+ ///
+ /// Unregisters a .
+ ///
+ /// The to unregister.
+ public static void Unregister(LocalizationProvider provider)
+ {
+ _providers.Remove(provider);
+ }
+
+ internal static bool TryGetTextFormatted(StringNames stringName, Il2CppReferenceArray parts, out string text)
+ {
+ foreach (var provider in _providers)
+ {
+ if (provider.TryGetTextFormatted(stringName, parts, out text!))
+ {
+ return true;
+ }
+ }
+
+ text = string.Empty;
+ return false;
+ }
+
+ internal static bool TryGetText(StringNames stringName, out string text)
+ {
+ foreach (var provider in _providers)
+ {
+ if (provider.TryGetText(stringName, out text!))
+ {
+ return true;
+ }
+ }
+
+ text = string.Empty;
+ return false;
+ }
+
+ internal static bool TryGetStringName(SystemTypes systemType, out StringNames stringName)
+ {
+ foreach (var provider in _providers)
+ {
+ if (provider.TryGetStringName(systemType, out var stringNameNullable))
+ {
+ stringName = stringNameNullable!.Value;
+ return true;
+ }
+ }
+
+ stringName = default;
+ return false;
+ }
+
+ internal static bool TryGetStringName(TaskTypes taskTypes, out StringNames stringName)
+ {
+ foreach (var provider in _providers)
+ {
+ if (provider.TryGetStringName(taskTypes, out var stringNameNullable))
+ {
+ stringName = stringNameNullable!.Value;
+ return true;
+ }
+ }
+
+ stringName = default;
+ return false;
+ }
+
+ internal static void OnLanguageChanged(SupportedLangs newLanguage)
+ {
+ foreach (var provider in _providers)
+ {
+ provider.SetLanguage(newLanguage);
+ }
+ }
+}
diff --git a/Reactor/Localization/LocalizationProvider.cs b/Reactor/Localization/LocalizationProvider.cs
new file mode 100644
index 0000000..ef341a3
--- /dev/null
+++ b/Reactor/Localization/LocalizationProvider.cs
@@ -0,0 +1,90 @@
+using Il2CppInterop.Runtime.InteropTypes.Arrays;
+using Reactor.Utilities;
+
+namespace Reactor.Localization;
+
+///
+/// The required implementation of a localization provider class.
+///
+public abstract class LocalizationProvider
+{
+ ///
+ /// Gets the current language Among Us is set to.
+ ///
+ /// If used from , this property will return the old language, not the new one.
+ ///
+ public SupportedLangs? CurrentLanguage { get; private set; }
+
+ ///
+ /// Gets the priority of this .
+ /// The higher the priority is, the earlier it will be invoked in relation to other providers.
+ ///
+ /// You can use the class for this value if you want to make this easier.
+ ///
+ public virtual int Priority => 0;
+
+ ///
+ /// Returns the localized text for the given .
+ ///
+ /// The to localize.
+ /// The representation of the given .
+ /// Whether or not this can handle this .
+ public virtual bool TryGetText(StringNames stringName, out string? result)
+ {
+ result = null;
+ return false;
+ }
+
+ ///
+ /// Returns the localized text for the given .
+ ///
+ /// The to localize.
+ /// The arguments used for formatting.
+ /// The representation of the given .
+ /// Whether or not this can handle this .
+ public virtual bool TryGetTextFormatted(StringNames stringName, Il2CppReferenceArray parts, out string? result)
+ {
+ if (!TryGetText(stringName, out result)) return false;
+
+ result = Il2CppSystem.String.Format(result, parts);
+ return true;
+ }
+
+ ///
+ /// Returns the equivalent for the given .
+ ///
+ /// The value.
+ /// The representation of the given .
+ /// Whether or not this can handle this .
+ public virtual bool TryGetStringName(SystemTypes systemType, out StringNames? result)
+ {
+ result = null;
+ return false;
+ }
+
+ ///
+ /// Returns the equivalent for the given .
+ ///
+ /// The value.
+ /// The representation of the given .
+ /// Whether or not this can handle this .
+ public virtual bool TryGetStringName(TaskTypes taskType, out StringNames? result)
+ {
+ result = null;
+ return false;
+ }
+
+ ///
+ /// This method is called when the Among Us language is changed by the user.
+ ///
+ /// The new language that Among Us has been set to.
+ public virtual void OnLanguageChanged(SupportedLangs newLanguage)
+ {
+ }
+
+ internal void SetLanguage(SupportedLangs newLanguage)
+ {
+ OnLanguageChanged(newLanguage);
+ CurrentLanguage = newLanguage;
+ }
+}
diff --git a/Reactor/Localization/Patches/GetStringPatch.cs b/Reactor/Localization/Patches/GetStringPatch.cs
new file mode 100644
index 0000000..aa765c5
--- /dev/null
+++ b/Reactor/Localization/Patches/GetStringPatch.cs
@@ -0,0 +1,85 @@
+using HarmonyLib;
+using Il2CppInterop.Runtime.InteropTypes.Arrays;
+
+namespace Reactor.Localization.Patches;
+
+[HarmonyPatch]
+internal static class GetStringPatch
+{
+ [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), typeof(StringNames), typeof(Il2CppReferenceArray))]
+ [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetStringWithDefault), typeof(StringNames), typeof(string), typeof(Il2CppReferenceArray))]
+ [HarmonyPrefix]
+ public static bool StringNamesPatch(StringNames id, Il2CppReferenceArray parts, out string __result)
+ {
+ if (LocalizationManager.TryGetTextFormatted(id, parts, out __result))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), typeof(SystemTypes))]
+ [HarmonyPrefix]
+ public static bool SystemTypesStringPatch(TranslationController __instance, SystemTypes room, ref string __result)
+ {
+ if (LocalizationManager.TryGetStringName(room, out var stringName))
+ {
+ if (LocalizationManager.TryGetText(stringName, out var text))
+ {
+ __result = text;
+ return false;
+ }
+
+ __result = __instance.GetString(stringName);
+ return false;
+ }
+
+ return true;
+ }
+
+ [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetSystemName))]
+ [HarmonyPrefix]
+ public static bool SystemTypesStringNamesPatch(SystemTypes room, ref StringNames __result)
+ {
+ if (LocalizationManager.TryGetStringName(room, out var stringName))
+ {
+ __result = stringName;
+ return false;
+ }
+
+ return true;
+ }
+
+ [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetString), typeof(TaskTypes))]
+ [HarmonyPrefix]
+ public static bool TaskTypesStringPatch(TranslationController __instance, TaskTypes task, ref string __result)
+ {
+ if (LocalizationManager.TryGetStringName(task, out var stringName))
+ {
+ if (LocalizationManager.TryGetText(stringName, out var text))
+ {
+ __result = text;
+ return false;
+ }
+
+ __result = __instance.GetString(stringName);
+ return false;
+ }
+
+ return true;
+ }
+
+ [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.GetTaskName))]
+ [HarmonyPrefix]
+ public static bool TaskTypesStringNamesPatch(TaskTypes task, ref StringNames __result)
+ {
+ if (LocalizationManager.TryGetStringName(task, out var stringName))
+ {
+ __result = stringName;
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Reactor/Localization/Patches/LanguageChangedPatch.cs b/Reactor/Localization/Patches/LanguageChangedPatch.cs
new file mode 100644
index 0000000..53fb806
--- /dev/null
+++ b/Reactor/Localization/Patches/LanguageChangedPatch.cs
@@ -0,0 +1,22 @@
+using HarmonyLib;
+
+namespace Reactor.Localization.Patches;
+
+[HarmonyPatch]
+internal static class LanguageChangedPatch
+{
+ [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.Initialize))]
+ [HarmonyPostfix]
+ public static void Initialize(TranslationController __instance)
+ {
+ if (TranslationController.Instance.GetInstanceID() == __instance.GetInstanceID())
+ LocalizationManager.OnLanguageChanged(__instance.currentLanguage.languageID);
+ }
+
+ [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.SetLanguage))]
+ [HarmonyPostfix]
+ public static void SetLanguage(TranslationController __instance)
+ {
+ LocalizationManager.OnLanguageChanged(__instance.currentLanguage.languageID);
+ }
+}
diff --git a/Reactor/Localization/Providers/HardCodedLocalizationProvider.cs b/Reactor/Localization/Providers/HardCodedLocalizationProvider.cs
new file mode 100644
index 0000000..8db5b69
--- /dev/null
+++ b/Reactor/Localization/Providers/HardCodedLocalizationProvider.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using Reactor.Utilities;
+
+namespace Reactor.Localization.Providers;
+
+///
+/// Utility for adding hard-coded localization.
+///
+public sealed class HardCodedLocalizationProvider : LocalizationProvider
+{
+ private static readonly Dictionary _strings = new();
+
+ ///
+ /// Adds a custom, hard-coded translation for a .
+ ///
+ /// The .
+ /// The text.
+ public static void Register(StringNames stringName, string value)
+ {
+ if (_strings.ContainsKey(stringName))
+ {
+ Warning($"Registering StringName {stringName} that already exists");
+ }
+
+ _strings[stringName] = value;
+ }
+
+ ///
+ public override int Priority => ReactorPriority.Low;
+
+ ///
+ public override bool TryGetText(StringNames stringName, out string? result)
+ {
+ return _strings.TryGetValue(stringName, out result);
+ }
+}
diff --git a/Reactor/Localization/Utilities/CustomStringName.cs b/Reactor/Localization/Utilities/CustomStringName.cs
new file mode 100644
index 0000000..e41236f
--- /dev/null
+++ b/Reactor/Localization/Utilities/CustomStringName.cs
@@ -0,0 +1,33 @@
+using Reactor.Localization.Providers;
+
+namespace Reactor.Localization.Utilities;
+
+///
+/// Provides a way to use custom static .
+///
+public static class CustomStringName
+{
+ private static int _lastId = int.MinValue + 1;
+
+ ///
+ /// Creates an returns a unique value.
+ ///
+ /// A unique .
+ public static StringNames Create()
+ {
+ var id = _lastId++;
+ return (StringNames) id;
+ }
+
+ ///
+ /// Creates, registeres and returns a unique value.
+ ///
+ /// The text.
+ /// A unique .
+ public static StringNames CreateAndRegister(string text)
+ {
+ var stringName = Create();
+ HardCodedLocalizationProvider.Register(stringName, text);
+ return stringName;
+ }
+}
diff --git a/Reactor/ReactorPlugin.cs b/Reactor/ReactorPlugin.cs
index 228bc3d..a007ca9 100644
--- a/Reactor/ReactorPlugin.cs
+++ b/Reactor/ReactorPlugin.cs
@@ -6,6 +6,8 @@
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using Il2CppInterop.Runtime.Attributes;
+using Reactor.Localization;
+using Reactor.Localization.Providers;
using Reactor.Networking;
using Reactor.Networking.Attributes;
using Reactor.Networking.Rpc;
@@ -44,11 +46,15 @@ public ReactorPlugin()
{
PluginSingleton.Instance = this;
PluginSingleton.Initialize();
+
RegisterInIl2CppAttribute.Initialize();
ModList.Initialize();
+
RegisterCustomRpcAttribute.Initialize();
MessageConverterAttribute.Initialize();
MethodRpcAttribute.Initialize();
+
+ LocalizationManager.Register(new HardCodedLocalizationProvider());
}
///
diff --git a/Reactor/Utilities/ReactorPriority.cs b/Reactor/Utilities/ReactorPriority.cs
new file mode 100644
index 0000000..3c68407
--- /dev/null
+++ b/Reactor/Utilities/ReactorPriority.cs
@@ -0,0 +1,52 @@
+namespace Reactor.Utilities;
+
+///
+/// Class containing common priorities to be used in various places across Reactor.
+///
+public static class ReactorPriority
+{
+ ///
+ /// Lowest priority, will happen last.
+ ///
+ public const int Last = -800;
+
+ ///
+ /// Very low priority.
+ ///
+ public const int VeryLow = -600;
+
+ ///
+ /// Low priority.
+ ///
+ public const int Low = -400;
+
+ ///
+ /// Lower than normal priority.
+ ///
+ public const int LowerThanNormal = -200;
+
+ ///
+ /// Normal priority.
+ ///
+ public const int Normal = 0;
+
+ ///
+ /// Higher than normal priority.
+ ///
+ public const int HigherThanNormal = 200;
+
+ ///
+ /// High priority.
+ ///
+ public const int High = 400;
+
+ ///
+ /// Very high priority.
+ ///
+ public const int VeryHigh = 600;
+
+ ///
+ /// Highest priority, will happen first.
+ ///
+ public const int First = 800;
+}