From 0a55c3510881da37818acc3f097df9d8c0da2904 Mon Sep 17 00:00:00 2001 From: Ape <155788660+ApeMV@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:37:51 +0100 Subject: [PATCH] Add custom options menu --- Modules/CustomOptionsHolder.cs | 46 ++ Modules/OptionBackup/OptionBackupData.cs | 71 +++ Modules/OptionBackup/OptionBackupValue.cs | 82 ++++ Modules/OptionItem/BooleanOptionItem.cs | 29 ++ Modules/OptionItem/FloatOptionItem.cs | 36 ++ Modules/OptionItem/IntegerOptionItem.cs | 36 ++ Modules/OptionItem/OptionItem.cs | 229 +++++++++ Modules/OptionItem/StringOptionItem.cs | 47 ++ Modules/OptionItem/TextOptionItem.cs | 22 + Modules/OptionItem/ValueRule.cs | 80 ++++ Modules/RPC.cs | 110 +++++ Modules/Utils.cs | 91 +++- Patches/GameOptionsMenuPatch.cs | 536 ++++++++++++++++++++++ Patches/GameSettingMenuPatch.cs | 183 ++++++++ Patches/LobbyViewSettingsPatch.cs | 162 +++++++ 15 files changed, 1756 insertions(+), 4 deletions(-) create mode 100644 Modules/CustomOptionsHolder.cs create mode 100644 Modules/OptionBackup/OptionBackupData.cs create mode 100644 Modules/OptionBackup/OptionBackupValue.cs create mode 100644 Modules/OptionItem/BooleanOptionItem.cs create mode 100644 Modules/OptionItem/FloatOptionItem.cs create mode 100644 Modules/OptionItem/IntegerOptionItem.cs create mode 100644 Modules/OptionItem/OptionItem.cs create mode 100644 Modules/OptionItem/StringOptionItem.cs create mode 100644 Modules/OptionItem/TextOptionItem.cs create mode 100644 Modules/OptionItem/ValueRule.cs create mode 100644 Modules/RPC.cs create mode 100644 Patches/GameOptionsMenuPatch.cs create mode 100644 Patches/GameSettingMenuPatch.cs create mode 100644 Patches/LobbyViewSettingsPatch.cs diff --git a/Modules/CustomOptionsHolder.cs b/Modules/CustomOptionsHolder.cs new file mode 100644 index 0000000..6aa768d --- /dev/null +++ b/Modules/CustomOptionsHolder.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using HarmonyLib; +using UnityEngine; +using System.Collections.Generic; + +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionHolder.cs +namespace HNSRevamped +{ + [HarmonyPatch] + public static class Options + { + static Task taskOptionsLoad; + + [HarmonyPatch(typeof(TranslationController), nameof(TranslationController.Initialize)), HarmonyPostfix] + public static void OptionsLoadStart() + { + taskOptionsLoad = Task.Run(Load); + } + + [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start)), HarmonyPostfix] + public static void WaitOptionsLoad() + { + taskOptionsLoad.Wait(); + } + + public static OptionItem Testy; + public static OptionItem Testy1; + public static OptionItem Testy2; + + + public static bool IsLoaded = false; + + public static void Load() + { + if (IsLoaded) return; + + //Main settings + Testy = IntegerOptionItem.Create(3, "Testy", new IntegerValueRule(0, 50, 5), 10, TabGroup.GameModifiers, false); + Testy1 = BooleanOptionItem.Create(4, "Testy1", true, TabGroup.ModSettings, false) + .SetColor(Color.red); + Testy2 = BooleanOptionItem.Create(5, "Testy2", true, TabGroup.ModSettings, false) + .SetParent(Testy1); + IsLoaded = true; + } + } +} \ No newline at end of file diff --git a/Modules/OptionBackup/OptionBackupData.cs b/Modules/OptionBackup/OptionBackupData.cs new file mode 100644 index 0000000..8c6a2c1 --- /dev/null +++ b/Modules/OptionBackup/OptionBackupData.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AmongUs.GameOptions; + +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionBackup/OptionBackupData.cs +namespace HNSRevamped +{ + public class OptionBackupData + { + public List AllValues; + public OptionBackupData(IGameOptions option) + { + AllValues = new(32); + + foreach (ByteOptionNames name in Enum.GetValues(typeof(ByteOptionNames))) + { + if (option.TryGetByte(name, out var value)) + AllValues.Add(new ByteOptionBackupValue(name, value)); + } + foreach (BoolOptionNames name in Enum.GetValues(typeof(BoolOptionNames))) + { + if (name == BoolOptionNames.GhostsDoTasks) continue; + if (option.TryGetBool(name, out var value)) + AllValues.Add(new BoolOptionBackupValue(name, value)); + } + foreach (FloatOptionNames name in Enum.GetValues(typeof(FloatOptionNames))) + { + if (option.TryGetFloat(name, out var value)) + AllValues.Add(new FloatOptionBackupValue(name, value)); + } + foreach (Int32OptionNames name in Enum.GetValues(typeof(Int32OptionNames))) + { + if (option.TryGetInt(name, out var value)) + AllValues.Add(new IntOptionBackupValue(name, value)); + } + AllValues.Add(new IntOptionBackupValue(Int32OptionNames.MaxPlayers, option.MaxPlayers)); + AllValues.Add(new UIntOptionBackupValue(UInt32OptionNames.Keywords, (uint)option.Keywords)); + + foreach (RoleTypes role in new RoleTypes[] { RoleTypes.Scientist, RoleTypes.Engineer, RoleTypes.GuardianAngel, RoleTypes.Shapeshifter, RoleTypes.Noisemaker, RoleTypes.Phantom, RoleTypes.Tracker, RoleTypes.Detective, RoleTypes.Viper }) + { + AllValues.Add(new RoleRateBackupValue(role, option.RoleOptions.GetNumPerGame(role), option.RoleOptions.GetChancePerGame(role))); + } + } + + public IGameOptions Restore(IGameOptions option) + { + foreach (var value in AllValues) + { + value.Restore(option); + } + return option; + } + + public byte GetByte(ByteOptionNames name) => Get(name); + public bool GetBool(BoolOptionNames name) => Get(name); + public float GetFloat(FloatOptionNames name) => Get(name); + public int GetInt(Int32OptionNames name) => Get(name); + public uint GetUInt(UInt32OptionNames name) => Get(name); + public TValue Get(TKey name) + where TKey : Enum + { + var value = AllValues + .OfType>() + .Where(val => val.OptionName.Equals(name)). + FirstOrDefault(); + + return value == null ? default : value.Value; + } + } +} \ No newline at end of file diff --git a/Modules/OptionBackup/OptionBackupValue.cs b/Modules/OptionBackup/OptionBackupValue.cs new file mode 100644 index 0000000..d7e0fee --- /dev/null +++ b/Modules/OptionBackup/OptionBackupValue.cs @@ -0,0 +1,82 @@ +using System; +using AmongUs.GameOptions; + +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionBackup/OptionBackupValue.cs +namespace HNSRevamped +{ + public abstract class OptionBackupValue + { + public abstract void Restore(IGameOptions option); + } + + public abstract class OptionBackupValueBase : OptionBackupValue + where NameT : Enum + { + public readonly NameT OptionName; + public readonly ValueT Value; + public OptionBackupValueBase(NameT name, ValueT value) + { + OptionName = name; + Value = value; + } + } + + public class ByteOptionBackupValue : OptionBackupValueBase + { + public ByteOptionBackupValue(ByteOptionNames name, byte value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetByte(OptionName, Value); + } + } + public class BoolOptionBackupValue : OptionBackupValueBase + { + public BoolOptionBackupValue(BoolOptionNames name, bool value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetBool(OptionName, Value); + } + } + public class FloatOptionBackupValue : OptionBackupValueBase + { + public FloatOptionBackupValue(FloatOptionNames name, float value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetFloat(OptionName, Value); + } + } + public class IntOptionBackupValue : OptionBackupValueBase + { + public IntOptionBackupValue(Int32OptionNames name, int value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetInt(OptionName, Value); + } + } + public class UIntOptionBackupValue : OptionBackupValueBase + { + public UIntOptionBackupValue(UInt32OptionNames name, uint value) : base(name, value) { } + public override void Restore(IGameOptions option) + { + option.SetUInt(OptionName, Value); + } + } + + public class RoleRateBackupValue : OptionBackupValue + { + public RoleTypes roleType; + public int maxCount; + public int chance; + + public RoleRateBackupValue(RoleTypes type, int maxCount, int chance) + { + this.roleType = type; + this.maxCount = maxCount; + this.chance = chance; + } + public override void Restore(IGameOptions option) + { + option.RoleOptions.SetRoleRate(roleType, maxCount, chance); + } + } +} \ No newline at end of file diff --git a/Modules/OptionItem/BooleanOptionItem.cs b/Modules/OptionItem/BooleanOptionItem.cs new file mode 100644 index 0000000..32f8036 --- /dev/null +++ b/Modules/OptionItem/BooleanOptionItem.cs @@ -0,0 +1,29 @@ +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionItems/BooleanOptionItem.cs +namespace HNSRevamped +{ + public class BooleanOptionItem : OptionItem + { + public BooleanOptionItem(int id, string name, bool defaultValue, TabGroup tab, bool isSingleValue) + : base(id, name, defaultValue ? 1 : 0, tab, isSingleValue) + { + } + public static BooleanOptionItem Create( + int id, string name, bool defaultValue, TabGroup tab, bool isSingleValue + ) + { + return new BooleanOptionItem( + id, name, defaultValue, tab, isSingleValue + ); + } + + public override string GetString() + { + return GetBool() ? "✓" : "X"; + } + + public override void SetValue(int value) + { + base.SetValue(value % 2 == 0 ? 0 : 1); + } + } +} \ No newline at end of file diff --git a/Modules/OptionItem/FloatOptionItem.cs b/Modules/OptionItem/FloatOptionItem.cs new file mode 100644 index 0000000..ae78b9b --- /dev/null +++ b/Modules/OptionItem/FloatOptionItem.cs @@ -0,0 +1,36 @@ +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionItems/FloatOptionItem.cs +namespace HNSRevamped +{ + public class FloatOptionItem : OptionItem + { + public FloatValueRule Rule; + + public FloatOptionItem(int id, string name, float defaultValue, TabGroup tab, bool isSingleValue, FloatValueRule rule) + : base(id, name, rule.GetNearestIndex(defaultValue), tab, isSingleValue) + { + Rule = rule; + } + public static FloatOptionItem Create( + int id, string name, FloatValueRule rule, float defaultValue, TabGroup tab, bool isSingleValue + ) + { + return new FloatOptionItem( + id, name, defaultValue, tab, isSingleValue, rule + ); + } + + public override int GetInt() => (int)Rule.GetValueByIndex(CurrentValue); + public override float GetFloat() => Rule.GetValueByIndex(CurrentValue); + public override string GetString() + { + return ApplyFormat(Rule.GetValueByIndex(CurrentValue).ToString()); + } + public override int GetValue() + => Rule.RepeatIndex(base.GetValue()); + + public override void SetValue(int value) + { + base.SetValue(Rule.RepeatIndex(value)); + } + } +} \ No newline at end of file diff --git a/Modules/OptionItem/IntegerOptionItem.cs b/Modules/OptionItem/IntegerOptionItem.cs new file mode 100644 index 0000000..8cdfb08 --- /dev/null +++ b/Modules/OptionItem/IntegerOptionItem.cs @@ -0,0 +1,36 @@ +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionItems/IntegerOptionItem.cs +namespace HNSRevamped +{ + public class IntegerOptionItem : OptionItem + { + public IntegerValueRule Rule; + + public IntegerOptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSingleValue, IntegerValueRule rule) + : base(id, name, rule.GetNearestIndex(defaultValue), tab, isSingleValue) + { + Rule = rule; + } + public static IntegerOptionItem Create( + int id, string name, IntegerValueRule rule, int defaultValue, TabGroup tab, bool isSingleValue + ) + { + return new IntegerOptionItem( + id, name, defaultValue, tab, isSingleValue, rule + ); + } + + public override int GetInt() => Rule.GetValueByIndex(CurrentValue); + public override float GetFloat() => Rule.GetValueByIndex(CurrentValue); + public override string GetString() + { + return ApplyFormat(Rule.GetValueByIndex(CurrentValue).ToString()); + } + public override int GetValue() + => Rule.RepeatIndex(base.GetValue()); + + public override void SetValue(int value) + { + base.SetValue(Rule.RepeatIndex(value)); + } + } +} \ No newline at end of file diff --git a/Modules/OptionItem/OptionItem.cs b/Modules/OptionItem/OptionItem.cs new file mode 100644 index 0000000..3577c28 --- /dev/null +++ b/Modules/OptionItem/OptionItem.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using BepInEx.Configuration; +using UnityEngine; + +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionItems/OptionItem.cs +namespace HNSRevamped +{ + public abstract class OptionItem + { + public static IReadOnlyList AllOptions => _allOptions; + private static List _allOptions = new(); + public static int CurrentPreset { get; set; } + + public int Id { get; } + public string Name { get; } + public int DefaultValue { get; } + public TabGroup Tab { get; } + public bool IsSingleValue { get; } + + public Color NameColor { get; protected set; } + public OptionFormat ValueFormat { get; protected set; } + public bool IsHeader { get; protected set; } + public bool IsHidden { get; protected set; } + public bool IsText { get; protected set; } + public Dictionary ReplacementDictionary + { + get => _replacementDictionary; + set + { + if (value == null) _replacementDictionary?.Clear(); + else _replacementDictionary = value; + } + } + private Dictionary _replacementDictionary; + + public int CurrentValue + { + get => GetValue(); + set => SetValue(value); + } + + public OptionItem Parent { get; private set; } + public List Children; + + public ConfigEntry CurrentEntry => + IsSingleValue ? singleEntry : AllConfigEntries[CurrentPreset]; + private ConfigEntry[] AllConfigEntries; + private ConfigEntry singleEntry; + + public OptionBehaviour OptionBehaviour; + + public event EventHandler UpdateValueEvent; + + public OptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSingleValue) + { + Id = id; + Name = name; + DefaultValue = defaultValue; + Tab = tab; + IsSingleValue = isSingleValue; + + NameColor = Color.white; + ValueFormat = OptionFormat.None; + IsHeader = false; + IsHidden = false; + IsText = false; + + Children = new(); + + AllConfigEntries = new ConfigEntry[10]; + if (Id == 0) + { + singleEntry = Main.Instance.Config.Bind("Current Preset", id.ToString(), DefaultValue); + CurrentPreset = singleEntry.Value; + } + else if (IsSingleValue) + { + singleEntry = Main.Instance.Config.Bind("SingleEntryOptions", id.ToString(), DefaultValue); + } + else + { + for (int i = 0; i < 1; i++) + { + AllConfigEntries[i] = Main.Instance.Config.Bind($"Preset{i + 1}", id.ToString(), DefaultValue); + } + } + + _allOptions.Add(this); + } + + public OptionItem Do(Action action) + { + action(this); + return this; + } + + public OptionItem SetColor(Color value) => Do(i => i.NameColor = value); + public OptionItem SetValueFormat(OptionFormat value) => Do(i => i.ValueFormat = value); + public OptionItem SetHeader(bool value) => Do(i => i.IsHeader = value); + public OptionItem SetHidden(bool value) => Do(i => i.IsHidden = value); + public OptionItem SetText(bool value) => Do(i => i.IsHidden = value); + + public OptionItem SetParent(OptionItem parent) => Do(i => + { + i.Parent = parent; + parent.SetChild(i); + }); + public OptionItem SetChild(OptionItem child) => Do(i => i.Children.Add(child)); + public OptionItem RegisterUpdateValueEvent(EventHandler handler) + => Do(i => UpdateValueEvent += handler); + + public OptionItem AddReplacement((string key, string value) kvp) + => Do(i => + { + ReplacementDictionary ??= new(); + ReplacementDictionary.Add(kvp.key, kvp.value); + }); + public OptionItem RemoveReplacement(string key) + => Do(i => ReplacementDictionary?.Remove(key)); + + public virtual string GetName(bool disableColor = false) + { + return disableColor ? + Name : + Utils.ColorString(NameColor, Name); + } + public virtual bool GetBool() => CurrentValue != 0 && (Parent == null || Parent.GetBool()); + public virtual int GetInt() => CurrentValue; + public virtual float GetFloat() => CurrentValue; + public virtual string GetString() + { + return ApplyFormat(CurrentValue.ToString()); + } + public virtual int GetValue() => CurrentEntry.Value; + + public virtual bool IsHiddenOn() + { + return IsHidden; + } + + public string ApplyFormat(string value) + { + if (ValueFormat == OptionFormat.Players) return value + " players"; + if (ValueFormat == OptionFormat.Seconds) return value + "s"; + if (ValueFormat == OptionFormat.Percent) return value + "%"; + if (ValueFormat == OptionFormat.Multiplier) return value + "x"; + return value; + } + + public virtual void Refresh() + { + if (OptionBehaviour is not null and StringOption opt) + { + opt.TitleText.text = GetName(); + opt.ValueText.text = GetString(); + opt.oldValue = opt.Value = CurrentValue; + } + } + public virtual void SetValue(int value) + { + int beforeValue = CurrentEntry.Value; + int afterValue = CurrentEntry.Value = value; + + CallUpdateValueEvent(beforeValue, afterValue); + Refresh(); + SyncAllOptions(); + } + + public static OptionItem operator ++(OptionItem item) + => item.Do(item => item.SetValue(item.CurrentValue + 1)); + public static OptionItem operator --(OptionItem item) + => item.Do(item => item.SetValue(item.CurrentValue - 1)); + + public static void SwitchPreset(int newPreset) + { + CurrentPreset = Math.Clamp(newPreset, 0, 1); + + foreach (var op in AllOptions) + op.Refresh(); + + SyncAllOptions(); + } + public static void SyncAllOptions() + { + if ( + PlayerControl.AllPlayerControls.Count <= 1 || + AmongUsClient.Instance.AmHost == false || + PlayerControl.LocalPlayer == null + ) return; + + GameManager.Instance.RpcSyncCustomOptions(); + } + + + private void CallUpdateValueEvent(int beforeValue, int currentValue) + { + if (UpdateValueEvent == null) return; + UpdateValueEvent(this, new UpdateValueEventArgs(beforeValue, currentValue)); + } + + public class UpdateValueEventArgs : EventArgs + { + public int CurrentValue { get; set; } + public int BeforeValue { get; set; } + public UpdateValueEventArgs(int beforeValue, int currentValue) + { + CurrentValue = currentValue; + BeforeValue = beforeValue; + } + } + } + + public enum TabGroup + { + ModSettings, + GameModifiers, + + } + + public enum OptionFormat + { + None, + Players, + Seconds, + Percent, + Multiplier, + } +} \ No newline at end of file diff --git a/Modules/OptionItem/StringOptionItem.cs b/Modules/OptionItem/StringOptionItem.cs new file mode 100644 index 0000000..855174a --- /dev/null +++ b/Modules/OptionItem/StringOptionItem.cs @@ -0,0 +1,47 @@ +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionItems/StringOptionItem.cs +namespace HNSRevamped +{ + public class StringOptionItem : OptionItem + { + public IntegerValueRule Rule; + public string[] Selections; + + public StringOptionItem(int id, string name, int defaultValue, TabGroup tab, bool isSingleValue, string[] selections) + : base(id, name, defaultValue, tab, isSingleValue) + { + Rule = (0, selections.Length - 1, 1); + Selections = selections; + } + public static StringOptionItem Create( + int id, string name, string[] selections, int defaultIndex, TabGroup tab, bool isSingleValue + ) + { + return new StringOptionItem( + id, name, defaultIndex, tab, isSingleValue, selections + ); + } + + public override int GetInt() => Rule.GetValueByIndex(CurrentValue); + public override float GetFloat() => Rule.GetValueByIndex(CurrentValue); + public override string GetString() + { + return Selections[Rule.GetValueByIndex(CurrentValue)]; + } + public int GetChance() + { + if (Selections.Length == 2) return CurrentValue * 100; + + var offset = 12 - Selections.Length; + var index = CurrentValue + offset; + var rate = index <= 1 ? index * 5 : (index - 1) * 10; + return rate; + } + public override int GetValue() + => Rule.RepeatIndex(base.GetValue()); + + public override void SetValue(int value) + { + base.SetValue(Rule.RepeatIndex(value)); + } + } +} \ No newline at end of file diff --git a/Modules/OptionItem/TextOptionItem.cs b/Modules/OptionItem/TextOptionItem.cs new file mode 100644 index 0000000..bd8b396 --- /dev/null +++ b/Modules/OptionItem/TextOptionItem.cs @@ -0,0 +1,22 @@ +// https://github.com/Yumenopai/TownOfHost_Y/blob/main/Modules/OptionItem/TextOptionItem.cs +namespace HNSRevamped +{ + public class TextOptionItem : OptionItem + { + public FloatValueRule Rule; + + public TextOptionItem(int id, string name, TabGroup tab) + : base(id, name, 0, tab, true) + { + IsText = true; + } + public static TextOptionItem Create( + int id, string name, TabGroup tab + ) + { + return new TextOptionItem( + id, name, tab + ); + } + } +} \ No newline at end of file diff --git a/Modules/OptionItem/ValueRule.cs b/Modules/OptionItem/ValueRule.cs new file mode 100644 index 0000000..42a4432 --- /dev/null +++ b/Modules/OptionItem/ValueRule.cs @@ -0,0 +1,80 @@ +using System; + +// https://github.com/tukasa0001/TownOfHost/blob/main/Modules/OptionItems/ValueRule.cs +namespace HNSRevamped +{ + public abstract class ValueRule + { + public T MinValue { get; protected set; } + public T MaxValue { get; protected set; } + public T Step { get; protected set; } + + public ValueRule(T minValue, T maxValue, T step) + { + MinValue = minValue; + MaxValue = maxValue; + Step = step; + } + public ValueRule((T, T, T) tuple) + : this(tuple.Item1, tuple.Item2, tuple.Item3) + { } + + public abstract int RepeatIndex(int value); + public abstract T GetValueByIndex(int index); + public abstract int GetNearestIndex(T num); + } + + public class IntegerValueRule : ValueRule + { + public IntegerValueRule(int minValue, int maxValue, int step) + : base(minValue, maxValue, step) { } + public IntegerValueRule((int, int, int) tuple) + : base(tuple) { } + + public static implicit operator IntegerValueRule((int, int, int) tuple) + => new(tuple); + + public override int RepeatIndex(int value) + { + int MaxIndex = (MaxValue - MinValue) / Step; + value = value % (MaxIndex + 1); + if (value < 0) value = MaxIndex; + return value; + } + + public override int GetValueByIndex(int index) + => RepeatIndex(index) * Step + MinValue; + + public override int GetNearestIndex(int num) + { + return (int)Math.Round((num - MinValue) / (float)Step); + } + } + + public class FloatValueRule : ValueRule + { + public FloatValueRule(float minValue, float maxValue, float step) + : base(minValue, maxValue, step) { } + public FloatValueRule((float, float, float) tuple) + : base(tuple) { } + + public static implicit operator FloatValueRule((float, float, float) tuple) + => new(tuple); + + public override int RepeatIndex(int value) + { + int MaxIndex = (int)((MaxValue - MinValue) / Step); + value = value % (MaxIndex + 1); + if (value < 0) value = MaxIndex; + return value; + } + + public override float GetValueByIndex(int index) + => RepeatIndex(index) * Step + MinValue; + + public override int GetNearestIndex(float num) + { + return (int)Math.Round((num - MinValue) / Step); + } + } +} \ No newline at end of file diff --git a/Modules/RPC.cs b/Modules/RPC.cs new file mode 100644 index 0000000..1149bd6 --- /dev/null +++ b/Modules/RPC.cs @@ -0,0 +1,110 @@ +using HarmonyLib; +using Hazel; +using InnerNet; +using UnityEngine; +using System; +using System.Linq; +using AmongUs.GameOptions; + +using Object = UnityEngine.Object; + +namespace HNSRevamped +{ + public enum CustomRPC + { + SyncCustomOptions, + AddCustomSettingsChangeMessage, + + } + + [HarmonyPatch(typeof(GameManager), nameof(GameManager.HandleRpc))] + class GameManagerHandleRpc + { + public static void Postfix(GameManager __instance, [HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + if (callId < 70) return; + var rpcType = (CustomRPC)callId; + switch (rpcType) + { + case CustomRPC.SyncCustomOptions: + for (int i = reader.ReadInt32(); i < OptionItem.AllOptions.Count; ++i) + { + if (reader.BytesRemaining == 0) break; + var co = OptionItem.AllOptions[i]; + if (co is TextOptionItem) continue; + if (co.Id == 0) + { + co.SetValue(9); + continue; + } + if (co.Id >= 1000 && co.IsHiddenOn()) continue; + co.SetValue(reader.ReadInt32()); + } + var viewSettingsPane = Object.FindObjectOfType(); + if (viewSettingsPane != null) + { + if (viewSettingsPane.currentTab != StringNames.OverviewCategory && viewSettingsPane.currentTab != StringNames.RolesCategory) + viewSettingsPane.ChangeTab(viewSettingsPane.currentTab); + LobbyViewPatch.ReCreateButtons(viewSettingsPane); + } + break; + case CustomRPC.AddCustomSettingsChangeMessage: + int optionId = reader.ReadInt32(); + var optionItem = OptionItem.AllOptions.FirstOrDefault(opt => opt.Id == optionId); + if (optionItem == null) break; + __instance.AddCustomSettingsChangeMessage(optionItem, reader.ReadString(), reader.ReadBoolean()); + break; + } + } + } + + static class RPC + { + + public static void AddCustomSettingsChangeMessage(this GameManager manager, OptionItem optionItem, string value, bool playSound) + { + string optionName = ""; + if (optionItem.Parent?.Parent?.Parent != null) + optionName += optionItem.Parent.Parent.Parent.GetOptionNameSCM() + " → "; + if (optionItem.Parent?.Parent != null) + optionName += optionItem.Parent.Parent.GetOptionNameSCM() + " → "; + if (optionItem.Parent != null) + optionName += optionItem.Parent.GetOptionNameSCM() + " → "; + optionName += optionItem.GetOptionNameSCM(); + string text = $"{optionName}: {value}"; + HudManager.Instance.Notifier.CustomSettingsChangeMessageLogic(optionItem, text, playSound); + } + + + public static void RpcSyncCustomOptions(this GameManager manager) + { + if (!AmongUsClient.Instance.AmHost) return; + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(manager.NetId, (byte)CustomRPC.SyncCustomOptions, SendOption.Reliable, -1); + writer.Write(0); + for (int i = 0; i < OptionItem.AllOptions.Count; ++i) + { + var co = OptionItem.AllOptions[i]; + if (co.Id == 0 || co is TextOptionItem) continue; + if (co.Id >= 1000 && co.IsHiddenOn()) continue; + if (writer.Length > 500) + { + AmongUsClient.Instance.FinishRpcImmediately(writer); + writer = AmongUsClient.Instance.StartRpcImmediately(manager.NetId, (byte)CustomRPC.SyncCustomOptions, SendOption.Reliable, -1); + writer.Write(i); + } + writer.Write(co.CurrentValue); + } + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + + public static void RpcAddCustomSettingsChangeMessage(this GameManager manager, OptionItem optionItem, string value, bool playSound) + { + manager.AddCustomSettingsChangeMessage(optionItem, value, playSound); + MessageWriter writer = AmongUsClient.Instance.StartRpcImmediately(manager.NetId, (byte)CustomRPC.AddCustomSettingsChangeMessage, SendOption.Reliable, -1); + writer.Write(optionItem.Id); + writer.Write(value); + writer.Write(playSound); + AmongUsClient.Instance.FinishRpcImmediately(writer); + } + } +} \ No newline at end of file diff --git a/Modules/Utils.cs b/Modules/Utils.cs index ea29a3f..6c06ce4 100644 --- a/Modules/Utils.cs +++ b/Modules/Utils.cs @@ -1,8 +1,91 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; using AmongUs.GameOptions; +using Hazel; +using UnityEngine; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using InnerNet; +using System.IO; +using System.Reflection; +using System; +using System.Security.Cryptography; +using System.Text; +using AmongUs.InnerNet.GameDataMessages; + +using Object = UnityEngine.Object; namespace HNSRevamped; -public static class Utils -{ - public static bool isHideNSeek => GameOptionsManager.Instance.CurrentGameOptions.GameMode == GameModes.HideNSeek; -} \ No newline at end of file + public static class Utils + { + public static bool isHideNSeek => GameOptionsManager.Instance.CurrentGameOptions.GameMode == GameModes.HideNSeek; + + public static string ColorString(Color32 color, string str) => $"<#{color.r:x2}{color.g:x2}{color.b:x2}{color.a:x2}>{str}"; + public static string ColorToHex(Color32 color) => $"#{color.r:x2}{color.g:x2}{color.b:x2}{color.a:x2}"; + + + + public static string GetTabName(TabGroup tab) + { + switch (tab) + { + case TabGroup.ModSettings: + return "Mod Settings"; + case TabGroup.GameModifiers: + return "Game Modifiers"; + + default: + return ""; + } + } + + public static void DestroyTranslator(this GameObject obj) + { + var translator = obj.GetComponent(); + if (translator != null) + { + Object.Destroy(translator); + } + } + + public static void DestroyTranslator(this MonoBehaviour obj) => obj.gameObject.DestroyTranslator(); + + public static void CustomSettingsChangeMessageLogic(this NotificationPopper notificationPopper, OptionItem optionItem, string text, bool playSound) + { + if (notificationPopper.lastMessageKey == 10000 + optionItem.Id && notificationPopper.activeMessages.Count > 0) + { + notificationPopper.activeMessages[notificationPopper.activeMessages.Count - 1].UpdateMessage(text); + } + else + { + notificationPopper.lastMessageKey = 10000 + optionItem.Id; + LobbyNotificationMessage settingmessage = Object.Instantiate(notificationPopper.notificationMessageOrigin, Vector3.zero, Quaternion.identity, notificationPopper.transform); + settingmessage.transform.localPosition = new Vector3(0f, 0f, -2f); + settingmessage.SetUp(text, notificationPopper.settingsChangeSprite, notificationPopper.settingsChangeColor, new Action(() => + { + notificationPopper.OnMessageDestroy(settingmessage); + })); + notificationPopper.ShiftMessages(); + notificationPopper.AddMessageToQueue(settingmessage); + } + if (playSound) + { + SoundManager.Instance.PlaySoundImmediate(notificationPopper.settingsChangeSound, false, 1f, 1f, null); + } + } + + public static string GetOptionNameSCM(this OptionItem optionItem) + { + if (optionItem.Name == "Enable") + { + int id = optionItem.Id; + while (id % 10 != 0) + --id; + var optionItem2 = OptionItem.AllOptions.FirstOrDefault(opt => opt.Id == id); + return optionItem2 != null ? optionItem2.GetName() : optionItem.GetName(); + } + else + return optionItem.GetName(); + } + } \ No newline at end of file diff --git a/Patches/GameOptionsMenuPatch.cs b/Patches/GameOptionsMenuPatch.cs new file mode 100644 index 0000000..8d4a65d --- /dev/null +++ b/Patches/GameOptionsMenuPatch.cs @@ -0,0 +1,536 @@ +using System; +using Il2CppSystem.Collections.Generic; +using HarmonyLib; +using UnityEngine; + +using Object = UnityEngine.Object; + +// https://github.com/Yumenopai/TownOfHost_Y/blob/main/Patches/GameOptionsPatch.cs +namespace HNSRevamped +{ + public static class ModGameOptionsMenu + { + public static int TabIndex = 0; + public static Dictionary OptionList = new(); + public static Dictionary BehaviourList = new(); + public static Dictionary CategoryHeaderList = new(); + } + + [HarmonyPatch(typeof(GameOptionsMenu))] + public static class GameOptionsMenuPatch + { + [HarmonyPatch(nameof(GameOptionsMenu.Initialize)), HarmonyPrefix] + private static bool InitializePrefix(GameOptionsMenu __instance) + { + if (ModGameOptionsMenu.TabIndex < 3) return true; + if (__instance.Children == null || __instance.Children.Count == 0) + { + __instance.MapPicker.gameObject.SetActive(false); + __instance.Children = new List(); + __instance.CreateSettings(); + __instance.cachedData = GameOptionsManager.Instance.CurrentGameOptions; + for (int i = 0; i < __instance.Children.Count; i++) + { + OptionBehaviour optionBehaviour = __instance.Children[i]; + optionBehaviour.OnValueChanged = new Action(__instance.ValueChanged); + } + __instance.InitializeControllerNavigation(); + } + return false; + } + [HarmonyPatch(nameof(GameOptionsMenu.CreateSettings)), HarmonyPrefix] + private static bool CreateSettingsPrefix(GameOptionsMenu __instance) + { + if (ModGameOptionsMenu.TabIndex < 3) return true; + var modTab = (TabGroup)(ModGameOptionsMenu.TabIndex - 3); + float num = 2.0f; + const float pos_x = 0.952f; + const float pos_z = -2.0f; + for (int index = 0; index < OptionItem.AllOptions.Count; index++) + { + var option = OptionItem.AllOptions[index]; + if (option.Tab != modTab) continue; + + var enabled = !option.IsHiddenOn() && (option.Parent == null || (!option.Parent.IsHiddenOn() && option.Parent.GetBool())); + if (option.IsHeader || option is TextOptionItem) + { + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); + categoryHeaderMasked.SetHeader(StringNames.RolesCategory, 20); + categoryHeaderMasked.Title.text = option.GetName(); + categoryHeaderMasked.transform.localScale = Vector3.one * 0.63f; + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, pos_z); + categoryHeaderMasked.transform.FindChild("HeaderText").GetComponent().fontStyle = TMPro.FontStyles.Bold; + categoryHeaderMasked.transform.FindChild("HeaderText").GetComponent().outlineWidth = 0.17f; + categoryHeaderMasked.gameObject.SetActive(enabled); + ModGameOptionsMenu.CategoryHeaderList.TryAdd(index, categoryHeaderMasked); + if (enabled) num -= 0.63f; + } + if (option is TextOptionItem) continue; + var baseGameSetting = GetSetting(option); + if (baseGameSetting == null) continue; + + OptionBehaviour optionBehaviour; + switch (baseGameSetting.Type) + { + case OptionTypes.Checkbox: + { + optionBehaviour = Object.Instantiate(__instance.checkboxOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(pos_x, num, pos_z); + OptionBehaviourSetSizeAndPosition(optionBehaviour, option, baseGameSetting.Type); + optionBehaviour.SetClickMask(__instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + ModGameOptionsMenu.OptionList.TryAdd(optionBehaviour, index); + break; + } + case OptionTypes.String: + { + optionBehaviour = Object.Instantiate(__instance.stringOptionOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(pos_x, num, pos_z); + OptionBehaviourSetSizeAndPosition(optionBehaviour, option, baseGameSetting.Type); + optionBehaviour.SetClickMask(__instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + ModGameOptionsMenu.OptionList.TryAdd(optionBehaviour, index); + break; + } + case OptionTypes.Float: + case OptionTypes.Int: + { + optionBehaviour = Object.Instantiate(__instance.numberOptionOrigin, Vector3.zero, Quaternion.identity, __instance.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(pos_x, num, pos_z); + OptionBehaviourSetSizeAndPosition(optionBehaviour, option, baseGameSetting.Type); + optionBehaviour.SetClickMask(__instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + ModGameOptionsMenu.OptionList.TryAdd(optionBehaviour, index); + break; + } + default: + continue; + } + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.SetClickMask(__instance.ButtonClickMask); + optionBehaviour.SetUpFromData(baseGameSetting, 20); + ModGameOptionsMenu.OptionList.TryAdd(optionBehaviour, index); + ModGameOptionsMenu.BehaviourList.TryAdd(index, optionBehaviour); + optionBehaviour.gameObject.SetActive(enabled); + __instance.Children.Add(optionBehaviour); + if (enabled) num -= 0.45f; + } + __instance.ControllerSelectable.Clear(); + foreach (var x in __instance.scrollBar.GetComponentsInChildren()) + __instance.ControllerSelectable.Add(x); + __instance.scrollBar.SetYBoundsMax(-num - 1.65f); + return false; + } + private static void OptionBehaviourSetSizeAndPosition(OptionBehaviour optionBehaviour, OptionItem option, OptionTypes type) + { + Vector3 positionOffset = new(0f, 0f, 0f); + Vector3 scaleOffset = new(0f, 0f, 0f); + Color color = new(0.7f, 0.7f, 0.7f); + float sizeDelta_x = 5.7f; + if (option.Parent?.Parent?.Parent != null) + { + scaleOffset = new(-0.18f, 0, 0); + positionOffset = new(0.3f, 0f, 0f); + color = new(0.7f, 0.5f, 0.5f); + sizeDelta_x = 5.1f; + } + else if (option.Parent?.Parent != null) + { + scaleOffset = new(-0.12f, 0, 0); + positionOffset = new(0.2f, 0f, 0f); + color = new(0.5f, 0.5f, 0.7f); + sizeDelta_x = 5.3f; + } + else if (option.Parent != null) + { + scaleOffset = new(-0.05f, 0, 0); + positionOffset = new(0.1f, 0f, 0f); + color = new(0.5f, 0.7f, 0.5f); + sizeDelta_x = 5.5f; + } + optionBehaviour.transform.FindChild("LabelBackground").GetComponent().color = color; + optionBehaviour.transform.FindChild("LabelBackground").localScale += new Vector3(0.9f, -0.2f, 0f) + scaleOffset; + optionBehaviour.transform.FindChild("LabelBackground").localPosition += new Vector3(-0.4f, 0f, 0f) + positionOffset; + optionBehaviour.transform.FindChild("Title Text").localPosition += new Vector3(-0.4f, 0f, 0f) + positionOffset; ; + optionBehaviour.transform.FindChild("Title Text").GetComponent().sizeDelta = new Vector2(sizeDelta_x, 0.37f); + optionBehaviour.transform.FindChild("Title Text").GetComponent().alignment = TMPro.TextAlignmentOptions.MidlineLeft; + optionBehaviour.transform.FindChild("Title Text").GetComponent().fontStyle = TMPro.FontStyles.Bold; + optionBehaviour.transform.FindChild("Title Text").GetComponent().outlineWidth = 0.17f; + switch (type) + { + case OptionTypes.Checkbox: + optionBehaviour.transform.FindChild("Toggle").localPosition = new Vector3(1.46f, -0.042f); + break; + case OptionTypes.String: + optionBehaviour.transform.FindChild("PlusButton").localPosition += new Vector3(1.7f, 0f, 0f); + optionBehaviour.transform.FindChild("MinusButton").localPosition += new Vector3(0.9f, 0f, 0f); + optionBehaviour.transform.FindChild("Value_TMP (1)").localPosition += new Vector3(1.3f, 0f, 0f); + optionBehaviour.transform.FindChild("Value_TMP (1)").GetComponent().sizeDelta = new Vector2(2.3f, 0.4f); + goto default; + case OptionTypes.Float: + case OptionTypes.Int: + optionBehaviour.transform.FindChild("PlusButton").localPosition += new Vector3(1.7f, 0f, 0f); + optionBehaviour.transform.FindChild("MinusButton").localPosition += new Vector3(0.9f, 0f, 0f); + optionBehaviour.transform.FindChild("Value_TMP").localPosition += new Vector3(1.3f, 0f, 0f); + goto default; + default: + optionBehaviour.transform.FindChild("ValueBox").localScale += new Vector3(0.2f, 0f, 0f); + optionBehaviour.transform.FindChild("ValueBox").localPosition += new Vector3(1.3f, 0f, 0f); + break; + } + } + + [HarmonyPatch(nameof(GameOptionsMenu.ValueChanged)), HarmonyPrefix] + private static bool ValueChangedPrefix(GameOptionsMenu __instance, OptionBehaviour option) + { + if (ModGameOptionsMenu.TabIndex < 3) return true; + if (ModGameOptionsMenu.OptionList.TryGetValue(option, out var index)) + { + var item = OptionItem.AllOptions[index]; + if (item != null) + { + if (item is BooleanOptionItem) + GameManager.Instance.RpcAddCustomSettingsChangeMessage(item, item.GetBool() ? "On" : "Off", true); + else + GameManager.Instance.RpcAddCustomSettingsChangeMessage(item, item.GetString(), true); + if (item.Id == 0) + { + ReOpenSettingMenu(); + var viewSettingsPane = Object.FindObjectOfType(); + if (viewSettingsPane != null) + LobbyViewPatch.ReCreateButtons(viewSettingsPane); + } + else if (item.Id == 2) + { + ReOpenSettingMenu(); + var viewSettingsPane = Object.FindObjectOfType(); + if (viewSettingsPane != null) + LobbyViewPatch.ReCreateButtons(viewSettingsPane); + } + else if (item.Children.Count > 0) + ReCreateSettings(__instance); + } + } + return false; + } + private static void ReCreateSettings(GameOptionsMenu __instance) + { + if (ModGameOptionsMenu.TabIndex < 3) return; + var modTab = (TabGroup)(ModGameOptionsMenu.TabIndex - 3); + float num = 2.0f; + for (int index = 0; index < OptionItem.AllOptions.Count; index++) + { + var option = OptionItem.AllOptions[index]; + if (option.Tab != modTab) continue; + var enabled = !option.IsHiddenOn() && (option.Parent == null || (!option.Parent.IsHiddenOn() && option.Parent.GetBool())); + + if (ModGameOptionsMenu.CategoryHeaderList.TryGetValue(index, out var categoryHeaderMasked)) + { + categoryHeaderMasked.transform.localPosition = new Vector3(-0.903f, num, -2f); + categoryHeaderMasked.gameObject.SetActive(enabled); + if (enabled) num -= 0.63f; + } + if (ModGameOptionsMenu.BehaviourList.TryGetValue(index, out var optionBehaviour)) + { + optionBehaviour.transform.localPosition = new Vector3(0.952f, num, -2f); + optionBehaviour.gameObject.SetActive(enabled); + if (enabled) num -= 0.45f; + } + } + __instance.ControllerSelectable.Clear(); + foreach (var x in __instance.scrollBar.GetComponentsInChildren()) + __instance.ControllerSelectable.Add(x); + __instance.scrollBar.SetYBoundsMax(-num - 1.65f); + } + + public static BaseGameSetting GetSetting(OptionItem item) + { + BaseGameSetting baseGameSetting = null; + if (item is BooleanOptionItem) + { + baseGameSetting = new CheckboxGameSetting + { + Type = OptionTypes.Checkbox, + }; + } + else if (item is IntegerOptionItem) + { + IntegerOptionItem intItem = item as IntegerOptionItem; + baseGameSetting = new IntGameSetting + { + Type = OptionTypes.Int, + Value = intItem.GetInt(), + Increment = intItem.Rule.Step, + ValidRange = new IntRange(intItem.Rule.MinValue, intItem.Rule.MaxValue), + ZeroIsInfinity = false, + SuffixType = NumberSuffixes.Multiplier, + FormatString = string.Empty, + }; + } + else if (item is FloatOptionItem) + { + FloatOptionItem floatItem = item as FloatOptionItem; + baseGameSetting = new FloatGameSetting + { + Type = OptionTypes.Float, + Value = floatItem.GetFloat(), + Increment = floatItem.Rule.Step, + ValidRange = new FloatRange(floatItem.Rule.MinValue, floatItem.Rule.MaxValue), + ZeroIsInfinity = false, + SuffixType = NumberSuffixes.Multiplier, + FormatString = string.Empty, + }; + } + else if (item is StringOptionItem) + { + StringOptionItem stringItem = item as StringOptionItem; + baseGameSetting = new StringGameSetting + { + Type = OptionTypes.String, + Values = new StringNames[stringItem.Selections.Length], + Index = stringItem.GetInt(), + }; + } + if (baseGameSetting != null) + baseGameSetting.Title = StringNames.Accept; + return baseGameSetting; + } + + public static void ReOpenSettingMenu() + { + var tab = ModGameOptionsMenu.TabIndex; + if (GameSettingMenu.Instance == null) return; + GameSettingMenu.Instance.Close(); + OptionsConsole optionsConsole = null; + foreach (var console in Object.FindObjectsOfType()) + { + if (console.HostOnly) + optionsConsole = console; + } + if (optionsConsole == null) return; + GameObject gameObject = Object.Instantiate(optionsConsole.MenuPrefab); + gameObject.transform.SetParent(Camera.main.transform, false); + gameObject.transform.localPosition = optionsConsole.CustomPosition; + new LateTask(() => { + if (GameSettingMenu.Instance == null) return; + GameSettingMenu.Instance.ChangeTab(tab, false); + }, 0f); + } + } + + [HarmonyPatch(typeof(ToggleOption))] + public static class ToggleOptionPatch + { + [HarmonyPatch(nameof(ToggleOption.Initialize)), HarmonyPrefix] + private static bool InitializePrefix(ToggleOption __instance) + { + if (ModGameOptionsMenu.OptionList.TryGetValue(__instance, out var index)) + { + var item = OptionItem.AllOptions[index]; + __instance.TitleText.text = item.GetName(); + __instance.CheckMark.enabled = item.GetBool(); + return false; + } + return true; + } + [HarmonyPatch(nameof(ToggleOption.Toggle)), HarmonyPrefix] + private static bool TogglePrefix(ToggleOption __instance) + { + if (ModGameOptionsMenu.OptionList.TryGetValue(__instance, out var index)) + { + __instance.CheckMark.enabled = !__instance.CheckMark.enabled; + var item = OptionItem.AllOptions[index]; + item.SetValue(__instance.GetBool() ? 1 : 0); + __instance.OnValueChanged.Invoke(__instance); + return false; + } + return true; + } + } + [HarmonyPatch(typeof(NumberOption))] + public static class NumberOptionPatch + { + [HarmonyPatch(nameof(NumberOption.Initialize)), HarmonyPrefix] + private static bool InitializePrefix(NumberOption __instance) + { + if (ModGameOptionsMenu.OptionList.TryGetValue(__instance, out var index)) + { + var item = OptionItem.AllOptions[index]; + __instance.TitleText.text = item.GetName(); + return false; + } + return true; + } + [HarmonyPatch(nameof(NumberOption.UpdateValue)), HarmonyPrefix] + private static bool UpdateValuePrefix(NumberOption __instance) + { + if (ModGameOptionsMenu.OptionList.TryGetValue(__instance, out var index)) + { + var item = OptionItem.AllOptions[index]; + if (item is IntegerOptionItem integerOptionItem) + { + integerOptionItem.SetValue(integerOptionItem.Rule.GetNearestIndex(__instance.GetInt())); + } + else if (item is FloatOptionItem floatOptionItem) + { + floatOptionItem.SetValue(floatOptionItem.Rule.GetNearestIndex(__instance.GetFloat())); + } + return false; + } + return true; + } + [HarmonyPatch(nameof(NumberOption.FixedUpdate)), HarmonyPrefix] + private static bool FixedUpdatePrefix(NumberOption __instance) + { + if (ModGameOptionsMenu.OptionList.TryGetValue(__instance, out var index)) + { + var item = OptionItem.AllOptions[index]; + if (__instance.oldValue != __instance.Value) + { + __instance.oldValue = __instance.Value; + __instance.ValueText.text = item.GetString(); + } + return false; + } + return true; + } + [HarmonyPatch(nameof(NumberOption.Increase)), HarmonyPrefix] + public static bool IncreasePrefix(NumberOption __instance) + { + if (__instance.Value == __instance.ValidRange.max) + { + __instance.Value = __instance.ValidRange.min; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + return false; + } + return true; + } + [HarmonyPatch(nameof(NumberOption.Decrease)), HarmonyPrefix] + public static bool DecreasePrefix(NumberOption __instance) + { + if (__instance.Value == __instance.ValidRange.min) + { + __instance.Value = __instance.ValidRange.max; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + return false; + } + return true; + } + [HarmonyPatch(nameof(NumberOption.AdjustButtonsActiveState)), HarmonyPrefix] + public static bool AdjustButtonsActiveStatePrefix(NumberOption __instance) + { + __instance.MinusBtn.SetInteractable(true); + __instance.PlusBtn.SetInteractable(true); + return false; + } + } + [HarmonyPatch(typeof(StringOption))] + public static class StringOptionPatch + { + [HarmonyPatch(nameof(StringOption.Initialize)), HarmonyPrefix] + private static bool InitializePrefix(StringOption __instance) + { + if (ModGameOptionsMenu.OptionList.TryGetValue(__instance, out var index)) + { + var item = OptionItem.AllOptions[index]; + __instance.TitleText.text = item.GetName(); + return false; + } + return true; + } + [HarmonyPatch(nameof(StringOption.UpdateValue)), HarmonyPrefix] + private static bool UpdateValuePrefix(StringOption __instance) + { + if (ModGameOptionsMenu.OptionList.TryGetValue(__instance, out var index)) + { + var item = OptionItem.AllOptions[index]; + item.SetValue(__instance.GetInt()); + return false; + } + return true; + } + [HarmonyPatch(nameof(StringOption.FixedUpdate)), HarmonyPrefix] + private static bool FixedUpdatePrefix(StringOption __instance) + { + if (ModGameOptionsMenu.OptionList.TryGetValue(__instance, out var index)) + { + var item = OptionItem.AllOptions[index]; + if (__instance.oldValue != __instance.Value) + { + __instance.oldValue = __instance.Value; + __instance.ValueText.text = item.GetString(); + } + return false; + } + return true; + } + [HarmonyPatch(nameof(StringOption.Increase)), HarmonyPrefix] + public static bool IncreasePrefix(StringOption __instance) + { + if (__instance.Value == __instance.Values.Length - 1) + { + __instance.Value = 0; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + return false; + } + return true; + } + [HarmonyPatch(nameof(StringOption.Decrease)), HarmonyPrefix] + public static bool DecreasePrefix(StringOption __instance) + { + if (__instance.Value == 0) + { + __instance.Value = __instance.Values.Length - 1; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + return false; + } + return true; + } + [HarmonyPatch(nameof(StringOption.AdjustButtonsActiveState)), HarmonyPrefix] + public static bool AdjustButtonsActiveStatePrefix(StringOption __instance) + { + __instance.MinusBtn.SetInteractable(true); + __instance.PlusBtn.SetInteractable(true); + return false; + } + } + [HarmonyPatch(typeof(PlayerOption))] + public static class PlayerOptionPatch + { + [HarmonyPatch(nameof(PlayerOption.Increase)), HarmonyPrefix] + public static bool IncreasePrefix(PlayerOption __instance) + { + if (__instance.Value == __instance.Values.Count - 1) + { + __instance.Value = -1; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + return false; + } + return true; + } + [HarmonyPatch(nameof(PlayerOption.Decrease)), HarmonyPrefix] + public static bool DecreasePrefix(PlayerOption __instance) + { + if (__instance.Value < 0) + { + __instance.Value = __instance.Values.Count - 1; + __instance.UpdateValue(); + __instance.OnValueChanged.Invoke(__instance); + return false; + } + return true; + } + [HarmonyPatch(nameof(PlayerOption.AdjustButtonsActiveState)), HarmonyPrefix] + public static bool AdjustButtonsActiveStatePrefix(PlayerOption __instance) + { + __instance.MinusBtn.SetInteractable(true); + __instance.PlusBtn.SetInteractable(true); + return false; + } + } +} \ No newline at end of file diff --git a/Patches/GameSettingMenuPatch.cs b/Patches/GameSettingMenuPatch.cs new file mode 100644 index 0000000..335395e --- /dev/null +++ b/Patches/GameSettingMenuPatch.cs @@ -0,0 +1,183 @@ +using System; +using HarmonyLib; +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using Object = UnityEngine.Object; + +// https://github.com/Yumenopai/TownOfHost_Y/blob/main/Patches/GameSettingMenuPatch.cs +namespace HNSRevamped +{ + [HarmonyPatch(typeof(GameSettingMenu))] + public class GameSettingMenuPatch + { + private static GameOptionsMenu templateGameOptionsMenu; + private static PassiveButton templateGameSettingsButton; + static Dictionary ModSettingsButtons = new(); + static Dictionary ModSettingsTabs = new(); + + [HarmonyPatch(nameof(GameSettingMenu.Start)), HarmonyPrefix] + [HarmonyPriority(Priority.First)] + public static void StartPostfix(GameSettingMenu __instance) + { + ModSettingsButtons = new(); + int tabNum = 0; + foreach (var tab in Enum.GetValues()) + { + + var button = Object.Instantiate(templateGameSettingsButton, __instance.GameSettingsButton.transform.parent); + button.gameObject.SetActive(true); + button.name = "Button_" + Utils.GetTabName(tab); + var label = button.GetComponentInChildren(); + label.DestroyTranslator(); + label.text = Utils.GetTabName(tab); + + Vector3 offset = new (0.0f, 0.5f * (tabNum / 2), 0.0f); + button.transform.localPosition = ((tabNum % 2 == 0) ? new Vector3(-3.9f, -0.9f, 0f) : new Vector3(-2.4f, -0.9f, 0f)) - offset; + button.transform.localScale = new(0.45f, 0.6f, 1f); + + var buttonComponent = button.GetComponent(); + buttonComponent.OnClick = new(); + buttonComponent.OnClick.AddListener((Action)(() => __instance.ChangeTab((int)tab + 3, false))); + ModSettingsButtons.Add(tab, button); + ++tabNum; + } + + ModGameOptionsMenu.OptionList = new(); + ModGameOptionsMenu.BehaviourList = new(); + ModGameOptionsMenu.CategoryHeaderList = new(); + + ModSettingsTabs = new(); + foreach (var tab in Enum.GetValues()) + { + var setTab = Object.Instantiate(templateGameOptionsMenu, __instance.GameSettingsTab.transform.parent); + setTab.name = Utils.GetTabName(tab); + setTab.gameObject.SetActive(false); + ModSettingsTabs.Add(tab, setTab); + } + foreach (var tab in Enum.GetValues()) + { + if (ModSettingsButtons.TryGetValue(tab, out var button)) + __instance.ControllerSelectable.Add(button); + } + } + private static void SetDefaultButton(GameSettingMenu __instance) + { + var gamepreset = __instance.GamePresetsButton; + gamepreset.gameObject.SetActive(false); + + var gamesettings = __instance.GameSettingsButton; + gamesettings.transform.localScale = new Vector3(0.45f, 0.6f, 1f); + gamesettings.transform.localPosition = new Vector3(-3.9f, -0.4f, 0f); + + var rolesettings = __instance.RoleSettingsButton; + rolesettings.transform.localScale = new Vector3(0.45f, 0.6f, 1f); + rolesettings.transform.localPosition = new(-2.4f, -0.4f, 0f); + var label = rolesettings.GetComponentInChildren(); + label.DestroyTranslator(); + label.text = "Vanilla Roles"; + } + + [HarmonyPatch(nameof(GameSettingMenu.ChangeTab)), HarmonyPrefix] + public static bool ChangeTabPrefix(GameSettingMenu __instance, ref int tabNum, [HarmonyArgument(1)] bool previewOnly) + { + if (!previewOnly) + ModGameOptionsMenu.TabIndex = tabNum; + GameOptionsMenu settingsTab; + PassiveButton button; + + if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly) + { + foreach (var tab in Enum.GetValues()) + { + if (ModSettingsTabs.TryGetValue(tab, out settingsTab) && settingsTab != null) + settingsTab.gameObject.SetActive(false); + } + foreach (var tab in Enum.GetValues()) + { + if (ModSettingsButtons.TryGetValue(tab, out button) && button != null) + button.SelectButton(false); + } + } + if (tabNum < 3) return true; + if ((previewOnly && Controller.currentTouchType == Controller.TouchType.Joystick) || !previewOnly) + { + __instance.PresetsTab.gameObject.SetActive(false); + __instance.GameSettingsTab.gameObject.SetActive(false); + __instance.RoleSettingsTab.gameObject.SetActive(false); + __instance.GamePresetsButton.SelectButton(false); + __instance.GameSettingsButton.SelectButton(false); + __instance.RoleSettingsButton.SelectButton(false); + + if (ModSettingsTabs.TryGetValue((TabGroup)(tabNum - 3), out settingsTab) && settingsTab != null) + { + settingsTab.gameObject.SetActive(true); + __instance.MenuDescriptionText.DestroyTranslator(); + switch ((TabGroup)(tabNum - 3)) + { + case TabGroup.ModSettings: + __instance.MenuDescriptionText.text = "Mod Settings"; + break; + case TabGroup.GameModifiers: + __instance.MenuDescriptionText.text = "Game Modifiers"; + break; + + } + } + } + if (previewOnly) + { + __instance.ToggleLeftSideDarkener(false); + __instance.ToggleRightSideDarkener(true); + return false; + } + __instance.ToggleLeftSideDarkener(true); + __instance.ToggleRightSideDarkener(false); + if (ModSettingsButtons.TryGetValue((TabGroup)(tabNum - 3), out button) && button != null) + button.SelectButton(true); + return false; + } + + [HarmonyPatch(nameof(GameSettingMenu.OnEnable)), HarmonyPrefix] + private static bool OnEnablePrefix(GameSettingMenu __instance) + { + if (templateGameOptionsMenu == null) + { + templateGameOptionsMenu = Object.Instantiate(__instance.GameSettingsTab, __instance.GameSettingsTab.transform.parent); + templateGameOptionsMenu.gameObject.SetActive(false); + } + if (templateGameSettingsButton == null) + { + templateGameSettingsButton = Object.Instantiate(__instance.GameSettingsButton, __instance.GameSettingsButton.transform.parent); + templateGameSettingsButton.gameObject.SetActive(false); + } + SetDefaultButton(__instance); + ControllerManager.Instance.OpenOverlayMenu(__instance.name, __instance.BackButton, __instance.DefaultButtonSelected, __instance.ControllerSelectable, false); + DestroyableSingleton.Instance.menuNavigationPrompts.SetActive(false); + if (Controller.currentTouchType != Controller.TouchType.Joystick) + __instance.ChangeTab(1, Controller.currentTouchType == Controller.TouchType.Joystick); + __instance.StartCoroutine(__instance.CoSelectDefault()); + return false; + } + + [HarmonyPatch(nameof(GameSettingMenu.Close)), HarmonyPostfix] + private static void ClosePostfix(GameSettingMenu __instance) + { + foreach (var button in ModSettingsButtons.Values) + Object.Destroy(button); + foreach (var tab in ModSettingsTabs.Values) + Object.Destroy(tab); + ModSettingsButtons = new(); + ModSettingsTabs = new(); + } + } + + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.RpcSyncSettings))] + public class RpcSyncSettingsPatch + { + public static void Postfix() + { + OptionItem.SyncAllOptions(); + } + } +} \ No newline at end of file diff --git a/Patches/LobbyViewSettingsPatch.cs b/Patches/LobbyViewSettingsPatch.cs new file mode 100644 index 0000000..455b023 --- /dev/null +++ b/Patches/LobbyViewSettingsPatch.cs @@ -0,0 +1,162 @@ +using System; +using TMPro; +using HarmonyLib; +using UnityEngine; +using System.Collections.Generic; + +using Object = UnityEngine.Object; + +namespace HNSRevamped +{ + [HarmonyPatch(typeof(LobbyViewSettingsPane))] + public class LobbyViewPatch + { + private static Dictionary ViewSettingsButtons = new(); + private static float num; + + [HarmonyPatch(nameof(LobbyViewSettingsPane.Awake))] + [HarmonyPostfix] + public static void AwakePostfix(LobbyViewSettingsPane __instance) + { + __instance.gameModeText.DestroyTranslator(); + + var OverviewButton = __instance.taskTabButton.gameObject; + OverviewButton.transform.localScale = new Vector3(0.4f, 1f, 1f); + OverviewButton.transform.localPosition += new Vector3(-1.1f, 0f, 0f); + + var RolesButton = __instance.rolesTabButton.gameObject; + RolesButton.transform.localScale = new Vector3(0.4f, 1f, 1f); + num = 1.4f; + RolesButton.transform.localPosition = OverviewButton.transform.localPosition + Vector3.right * num * 1f; + var label = RolesButton.transform.Find("FontPlacer").GetComponentInChildren(); + label.DestroyTranslator(); + label.text = "Vanilla Roles"; + + ViewSettingsButtons = new(); + num = 2.8f; + + } + + [HarmonyPatch(nameof(LobbyViewSettingsPane.ChangeTab))] + [HarmonyPrefix] + public static bool ChangeTabPrefix(LobbyViewSettingsPane __instance, [HarmonyArgument(0)] StringNames category) + { + if (category == StringNames.OverviewCategory || category == StringNames.RolesCategory) + { + foreach (var button in ViewSettingsButtons.Values) + button.GetComponent().SelectButton(false); + return true; + } + __instance.currentTab = category; + for (int i = 0; i < __instance.settingsInfo.Count; i++) + { + Object.Destroy(__instance.settingsInfo[i].gameObject); + } + __instance.settingsInfo.Clear(); + __instance.taskTabButton.SelectButton(false); + __instance.rolesTabButton.SelectButton(false); + foreach (var tab in ViewSettingsButtons.Keys) + { + if (tab == (TabGroup)__instance.currentTab) + ViewSettingsButtons[tab].GetComponent().SelectButton(true); + else + ViewSettingsButtons[tab].GetComponent().SelectButton(false); + } + DrawOptions(__instance, (TabGroup)__instance.currentTab); + __instance.scrollBar.ScrollToTop(); + return false; + } + + public static void DrawOptions(LobbyViewSettingsPane __instance, TabGroup tab) + { + float num = 1.44f; + int settingsCount = 0; + foreach (var option in OptionItem.AllOptions) + { + if (option.IsHiddenOn() || option.Tab != tab || (option.Parent != null && (option.Parent.IsHiddenOn() || !option.Parent.GetBool()))) continue; + if (option.IsHeader || option is TextOptionItem) + { + num -= 0.85f; + CategoryHeaderMasked categoryHeaderMasked = Object.Instantiate(__instance.categoryHeaderOrigin); + categoryHeaderMasked.SetHeader(StringNames.None, 61); + categoryHeaderMasked.transform.SetParent(__instance.settingsContainer); + categoryHeaderMasked.transform.localScale = Vector3.one; + categoryHeaderMasked.transform.localPosition = new Vector3(-9.77f, num, -2f); + __instance.settingsInfo.Add(categoryHeaderMasked.gameObject); + num -= 1f; + categoryHeaderMasked.DestroyTranslator(); + categoryHeaderMasked.Title.text = option.GetName(); + settingsCount = 0; + } + if (option is TextOptionItem) continue; + + ViewSettingsInfoPanel viewSettingsInfoPanel = Object.Instantiate(__instance.infoPanelOrigin); + viewSettingsInfoPanel.transform.SetParent(__instance.settingsContainer); + viewSettingsInfoPanel.transform.localScale = Vector3.one; + float num2; + if (settingsCount % 2 == 0) + { + num2 = -8.95f; + if (settingsCount > 0) + { + num -= 0.85f; + } + } + else + { + num2 = -3f; + } + viewSettingsInfoPanel.transform.localPosition = new Vector3(num2, num, -2f); + + if (option is BooleanOptionItem) + { + viewSettingsInfoPanel.SetInfoCheckbox(StringNames.None, 61, option.GetBool()); + } + else + { + viewSettingsInfoPanel.SetInfo(StringNames.None, option.GetString(), 61); + } + viewSettingsInfoPanel.titleText.text = option.GetName(); + + if (option.Parent?.Parent?.Parent != null) + { + viewSettingsInfoPanel.transform.FindChild("LabelBackground").GetComponent().color = new(0.7f, 0.5f, 0.5f); + } + else if (option.Parent?.Parent != null) + { + viewSettingsInfoPanel.transform.FindChild("LabelBackground").GetComponent().color = new(0.5f, 0.5f, 0.7f); + } + else if (option.Parent != null) + { + viewSettingsInfoPanel.transform.FindChild("LabelBackground").GetComponent().color = new(0.5f, 0.7f, 0.5f); + } + + __instance.settingsInfo.Add(viewSettingsInfoPanel.gameObject); + ++settingsCount; + } + __instance.scrollBar.SetYBoundsMax(-num - 1.44f); + } + + public static void ReCreateButtons(LobbyViewSettingsPane __instance) + { + foreach (var button in ViewSettingsButtons.Values) + Object.Destroy(button.gameObject); + ViewSettingsButtons = new(); + var OverviewButton = __instance.taskTabButton.gameObject; + var RolesButton = __instance.rolesTabButton.gameObject; + OverviewButton.transform.localScale = new Vector3(0.4f, 1f, 1f); + RolesButton.transform.localScale = new Vector3(0.4f, 1f, 1f); + num = 1.4f; + RolesButton.transform.localPosition = OverviewButton.transform.localPosition + Vector3.right * num * 1f; + num = 2.8f; + + } + + [HarmonyPatch(nameof(LobbyViewSettingsPane.OnEnable))] + [HarmonyPostfix] + public static void OnEnablePostfix(LobbyViewSettingsPane __instance) + { + ReCreateButtons(__instance); + } + } +}