From 6cdf3bda781d93592865aad22bcba0ffca33ab8b Mon Sep 17 00:00:00 2001 From: FangkuaiYa <2683748223@qq.com> Date: Wed, 20 Aug 2025 18:21:54 +0800 Subject: [PATCH 1/4] Update 1.9.0 --- .github/workflows/main.yml | 32 +- .gitignore | 80 +- .../RegisterCustomGameModeAttribute.cs | 8 +- .../Components/RegisterCustomRoleAttribute.cs | 14 +- PeasAPI/CustomButtons/CustomButton.cs | 65 +- PeasAPI/CustomEndReason/CustomEndReason.cs | 15 +- PeasAPI/CustomEndReason/EndReasonManager.cs | 26 +- PeasAPI/CustomRpc/RpcCustomCheckColor.cs | 4 +- PeasAPI/CustomRpc/RpcCustomEndReason.cs | 12 +- PeasAPI/CustomRpc/RpcInitializeRoles.cs | 28 +- PeasAPI/CustomRpc/RpcResetRoles.cs | 5 +- PeasAPI/CustomRpc/RpcSetColor.cs | 4 +- PeasAPI/CustomRpc/RpcSetRole.cs | 6 +- PeasAPI/CustomRpc/RpcSetVanillaRole.cs | 7 +- PeasAPI/CustomRpc/RpcShowMessage.cs | 4 +- PeasAPI/CustomRpc/RpcUpdateSetting.cs | 174 ++-- PeasAPI/Data.cs | 128 +-- PeasAPI/Enums/FileType.cs | 4 +- PeasAPI/Extensions.cs | 103 +-- PeasAPI/GameModes/GameMode.cs | 6 +- PeasAPI/GameModes/Patches.cs | 50 +- PeasAPI/Managers/CustomColorManager.cs | 27 +- PeasAPI/Managers/CustomHatManager.cs | 59 -- PeasAPI/Managers/CustomServerManager.cs | 84 +- PeasAPI/Managers/PlayerMenuManager.cs | 15 +- PeasAPI/Managers/TextMessageManager.cs | 9 +- PeasAPI/Managers/UpdateManager.cs | 3 +- PeasAPI/Managers/UpdateTools/GitHubUpdater.cs | 1 - .../Managers/UpdateTools/UpdateListener.cs | 23 +- PeasAPI/Managers/WatermarkManager.cs | 106 ++- PeasAPI/Options/CustomHeaderOption.cs | 15 + PeasAPI/Options/CustomNumberOption.cs | 148 +-- PeasAPI/Options/CustomOption.cs | 121 ++- PeasAPI/Options/CustomOptionButton.cs | 113 --- PeasAPI/Options/CustomOptionHeader.cs | 34 - PeasAPI/Options/CustomOptionType.cs | 26 + PeasAPI/Options/CustomRoleOption.cs | 190 ++-- PeasAPI/Options/CustomStringOption.cs | 140 +-- PeasAPI/Options/CustomToggleOption.cs | 149 +-- PeasAPI/Options/OptionManager.cs | 19 - PeasAPI/Options/Patches.cs | 850 ++++++++++++------ PeasAPI/Patches.cs | 5 +- PeasAPI/PeasAPI.cs | 14 +- PeasAPI/PeasAPI.csproj | 20 +- PeasAPI/Roles/BaseRole.cs | 67 +- PeasAPI/Roles/ModRole.cs | 86 ++ PeasAPI/Roles/Patches.cs | 136 ++- PeasAPI/Roles/RoleManager.cs | 27 +- PeasAPI/Utility.cs | 17 +- build.cake | 32 - PeasAPI/nuget.config => nuget.config | 2 + 51 files changed, 1553 insertions(+), 1760 deletions(-) delete mode 100644 PeasAPI/Managers/CustomHatManager.cs create mode 100644 PeasAPI/Options/CustomHeaderOption.cs delete mode 100644 PeasAPI/Options/CustomOptionButton.cs delete mode 100644 PeasAPI/Options/CustomOptionHeader.cs create mode 100644 PeasAPI/Options/CustomOptionType.cs delete mode 100644 PeasAPI/Options/OptionManager.cs create mode 100644 PeasAPI/Roles/ModRole.cs delete mode 100644 build.cake rename PeasAPI/nuget.config => nuget.config (56%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0395685..373d7ec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,29 +4,37 @@ on: [ "push", "pull_request" ] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/cache@v4 + with: + path: | + ~/.nuget/packages + ~/.cache/bepinex + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- + - uses: actions/checkout@v4 with: submodules: true - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: 6.x - - name: Run the Cake script - uses: cake-build/cake-action@v1 - with: - verbosity: Diagnostic + - name: Build + run: dotnet build PeasAPI/PeasAPI.csproj --configuration Release - - uses: actions/upload-artifact@v2 + - name: Upload PeasAPI + uses: actions/upload-artifact@v4 with: name: PeasAPI.dll - path: PeasAPI/bin/Release/netstandard2.1/PeasAPI.dll - - - uses: actions/upload-artifact@v2 + path: PeasAPI/bin/Release/net6.0/PeasAPI.dll + + - name: Upload PeasAPI.nupkg + uses: actions/upload-artifact@v4 with: name: PeasAPI.nupkg - path: PeasAPI/bin/Release/PeasAPI.*.nupkg + path: PeasAPI/bin/Release/PeasAPI.*.nupkg \ No newline at end of file diff --git a/.gitignore b/.gitignore index 95c2c8b..e54effa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +.idea/ + # User-specific files *.rsuser *.suo @@ -13,9 +15,6 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs -# Mono auto generated files -mono_crash.* - # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -29,7 +28,6 @@ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ -[Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ @@ -43,10 +41,9 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUnit +# NUNIT *.VisualState.xml TestResult.xml -nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -127,6 +124,9 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user +# JustCode is a .NET coding add-in +.JustCode + # TeamCity is a build add-in _TeamCity* @@ -184,8 +184,6 @@ PublishScripts/ # NuGet Packages *.nupkg -# NuGet Symbol Packages -*.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -210,8 +208,6 @@ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx -*.appxbundle -*.appxupload # Visual Studio cache files # files ending in .cache can be ignored @@ -261,9 +257,7 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl +*- Backup*.rdl # Microsoft Fakes FakesAssemblies/ @@ -299,6 +293,10 @@ paket-files/ # FAKE - F# Make .fake/ +# JetBrains Rider +.idea/ +*.sln.iml + # CodeRush personal settings .cr/personal @@ -341,58 +339,4 @@ ASALocalRun/ .localhistory/ # BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - - -# Jetbrains Resharper/Rider - -# Common IntelliJ Platform excludes - -# User specific -**/.idea/ - -# Sensitive or high-churn files -**/.idea/**/dataSources/ -**/.idea/**/dataSources.ids -**/.idea/**/dataSources.xml -**/.idea/**/dataSources.local.xml -**/.idea/**/sqlDataSources.xml -**/.idea/**/dynamic.xml - -# Rider -# Rider auto-generates .iml files, and contentModel.xml -**/.idea/**/*.iml -**/.idea/**/contentModel.xml -**/.idea/**/modules.xml - -*.suo -*.user -.vs/ -[Bb]in/ -[Oo]bj/ -_UpgradeReport_Files/ -[Pp]ackages/ - -Thumbs.db -Desktop.ini -.DS_Store - - - -# Visual Studio Code - -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ \ No newline at end of file +healthchecksdb \ No newline at end of file diff --git a/PeasAPI/Components/RegisterCustomGameModeAttribute.cs b/PeasAPI/Components/RegisterCustomGameModeAttribute.cs index 6ab7acb..06a246e 100644 --- a/PeasAPI/Components/RegisterCustomGameModeAttribute.cs +++ b/PeasAPI/Components/RegisterCustomGameModeAttribute.cs @@ -1,9 +1,8 @@ using System; using System.Reflection; -using BepInEx.IL2CPP; +using BepInEx.Unity.IL2CPP; using HarmonyLib; using PeasAPI.GameModes; -using Reactor; namespace PeasAPI.Components { @@ -38,10 +37,7 @@ public static void Register(Assembly assembly, BasePlugin plugin) public static void Load() { - ChainloaderHooks.PluginLoad += plugin => - { - Register(plugin.GetType().Assembly, plugin); - }; + IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => Register(assembly, plugin); } } } \ No newline at end of file diff --git a/PeasAPI/Components/RegisterCustomRoleAttribute.cs b/PeasAPI/Components/RegisterCustomRoleAttribute.cs index 97e8cf7..1940d81 100644 --- a/PeasAPI/Components/RegisterCustomRoleAttribute.cs +++ b/PeasAPI/Components/RegisterCustomRoleAttribute.cs @@ -1,9 +1,8 @@ using System; using System.Reflection; -using BepInEx.IL2CPP; +using BepInEx.Unity.IL2CPP; using HarmonyLib; using PeasAPI.Roles; -using Reactor; namespace PeasAPI.Components { @@ -20,18 +19,19 @@ public static void Register(Assembly assembly, BasePlugin plugin) { foreach (var type in assembly.GetTypes()) { - var attribute = type.GetCustomAttribute(); + var attribute = type.GetCustomAttribute(); if (attribute != null) { if (!type.IsSubclassOf(typeof(BaseRole))) { - throw new InvalidOperationException($"Type {type.FullDescription()} must extend {nameof(BaseRole)}."); + throw new InvalidOperationException( + $"Type {type.FullDescription()} must extend {nameof(BaseRole)}."); } - + if (PeasAPI.Logging) PeasAPI.Logger.LogInfo($"Registered role {type.Name} from {type.Assembly.GetName().Name}"); - + Activator.CreateInstance(type, plugin); } } @@ -39,7 +39,7 @@ public static void Register(Assembly assembly, BasePlugin plugin) public static void Load() { - ChainloaderHooks.PluginLoad += plugin => Register(plugin.GetType().Assembly, plugin); + IL2CPPChainloader.Instance.PluginLoad += (pluginInfo, assembly, plugin) => Register(assembly, plugin); } } } \ No newline at end of file diff --git a/PeasAPI/CustomButtons/CustomButton.cs b/PeasAPI/CustomButtons/CustomButton.cs index 9c9c6e7..c5de091 100644 --- a/PeasAPI/CustomButtons/CustomButton.cs +++ b/PeasAPI/CustomButtons/CustomButton.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; using UnityEngine; using Action = System.Action; @@ -13,10 +14,10 @@ public class CustomButton public static List Buttons = new List(); public static List VisibleButtons => Buttons.Where(button => button.Visible && button.CouldBeUsed()).ToList(); public static bool HudActive = true; - + private Color _startColorText = new Color(255, 255, 255); private Sprite _buttonSprite; - + public KillButton KillButtonManager; public Vector2 PositionOffset; public Vector2 TextOffset; @@ -105,19 +106,19 @@ private void Start() KillButtonManager.gameObject.SetActive(true); KillButtonManager.gameObject.name = "CustomButton"; KillButtonManager.transform.localScale = new Vector3(1, 1, 1); - + _startColorText = KillButtonManager.cooldownTimerText.color; - + KillButtonManager.graphic.enabled = true; KillButtonManager.graphic.sprite = _buttonSprite; - + KillButtonManager.buttonLabelText.enabled = UseText; KillButtonManager.buttonLabelText.text = Text; - KillButtonManager.buttonLabelText.transform.position += (Vector3) TextOffset + new Vector3(0f, 0.1f); - + KillButtonManager.buttonLabelText.transform.position += (Vector3)TextOffset + new Vector3(0f, 0.1f); + var button = KillButtonManager.GetComponent(); button.OnClick.RemoveAllListeners(); - button.OnClick.AddListener((UnityEngine.Events.UnityAction) listener); + button.OnClick.AddListener((UnityEngine.Events.UnityAction)listener); void listener() { @@ -137,7 +138,7 @@ void listener() } } } - + private void Update() { if (Target == TargetType.Player) @@ -167,7 +168,7 @@ private void Update() image = ObjectTarget.transform.FindChild("Sprite").GetComponent(); if (!image) image = ObjectTarget.GetComponentInChildren(); - + if (image) { image.material.SetFloat("_Outline", 0); @@ -181,7 +182,7 @@ private void Update() image = target.transform.FindChild("Sprite").GetComponent(); if (!image) image = target.GetComponentInChildren(); - + if (image) { image.material.SetFloat("_Outline", 1); @@ -202,7 +203,7 @@ private void Update() image = ObjectTarget.transform.FindChild("Sprite").GetComponent(); if (!image) image = ObjectTarget.GetComponentInChildren(); - + if (image) { image.material.SetFloat("_Outline", 0); @@ -216,7 +217,7 @@ private void Update() image = target.transform.FindChild("Sprite").GetComponent(); if (!image) image = target.GetComponentInChildren(); - + if (image) { image.material.SetFloat("_Outline", 1); @@ -248,7 +249,7 @@ private void Update() { KillButtonManager.cooldownTimerText.color = _startColorText; Cooldown = MaxCooldown; - + IsEffectActive = false; OnEffectEnd(); } @@ -257,16 +258,16 @@ private void Update() { if (CouldBeUsed() && Enabled) Cooldown -= Time.deltaTime; - + KillButtonManager.buttonLabelText.color = KillButtonManager.graphic.color = new Color(1f, 1f, 1f, 0.3f); } KillButtonManager.buttonLabelText.enabled = UseText; KillButtonManager.buttonLabelText.text = Text; - + KillButtonManager.gameObject.SetActive(CouldBeUsed()); KillButtonManager.graphic.enabled = CouldBeUsed(); - + if (CouldBeUsed()) { KillButtonManager.graphic.material.SetFloat("_Desat", 0f); @@ -277,13 +278,13 @@ private void Update() public bool CouldBeUsed() { - if (PlayerControl.LocalPlayer == null) + if (PlayerControl.LocalPlayer == null) return false; - - if (PlayerControl.LocalPlayer.Data == null) + + if (PlayerControl.LocalPlayer.Data == null) return false; - - if (MeetingHud.Instance != null) + + if (MeetingHud.Instance != null) return false; return _CouldBeUsed.Invoke(PlayerControl.LocalPlayer); @@ -296,7 +297,7 @@ public bool CanBeUsed() flag = PlayerTarget == null && ObjectTarget == null; return _CanBeUsed.Invoke(PlayerControl.LocalPlayer) && Usable && Cooldown < 0f && HudActive && !flag; } - + public void SetImage(Sprite image) { _buttonSprite = image; @@ -309,7 +310,7 @@ public void SetCoolDown(float cooldown, float? maxCooldown = null) MaxCooldown = maxCooldown.Value; KillButtonManager.SetCoolDown(Cooldown, MaxCooldown); } - + public bool IsCoolingDown() { return KillButtonManager.isCoolingDown; @@ -319,7 +320,7 @@ public PlayerControl FindClosestPlayer() { var from = PlayerControl.LocalPlayer; PlayerControl result = null; - float num = GameOptionsData.KillDistances[Mathf.Clamp(PlayerControl.GameOptions.KillDistance, 0, 2)]; + float num = LegacyGameOptions.KillDistances[Mathf.Clamp(GameOptionsManager.Instance.currentNormalGameOptions.KillDistance, 0, 2)]; if (!ShipStatus.Instance) { return null; @@ -355,8 +356,8 @@ public GameObject FindClosestObject() var from = PlayerControl.LocalPlayer; GameObject result1 = null; GameObject result2 = null; - float num1 = GameOptionsData.KillDistances[Mathf.Clamp(PlayerControl.GameOptions.KillDistance, 0, 2)]; - float num2 = GameOptionsData.KillDistances[Mathf.Clamp(PlayerControl.GameOptions.KillDistance, 0, 2)]; + float num1 = LegacyGameOptions.KillDistances[Mathf.Clamp(GameOptionsManager.Instance.currentNormalGameOptions.KillDistance, 0, 2)]; + float num2 = LegacyGameOptions.KillDistances[Mathf.Clamp(GameOptionsManager.Instance.currentNormalGameOptions.KillDistance, 0, 2)]; if (!ShipStatus.Instance) { return null; @@ -415,11 +416,11 @@ public static void Prefix(HudManager __instance) var button = Buttons[i]; var killButton = button.KillButtonManager; var canUse = button.CouldBeUsed(); - + Buttons[i].KillButtonManager.graphic.sprite = button._buttonSprite; - + killButton.gameObject.SetActive(button.Visible && canUse); - + killButton.buttonLabelText.enabled = canUse; killButton.buttonLabelText.alpha = killButton.isCoolingDown ? Palette.DisabledClear.a : Palette.EnabledColor.a; @@ -429,8 +430,8 @@ public static void Prefix(HudManager __instance) } } } - - [HarmonyPatch(typeof(HudManager), nameof(HudManager.SetHudActive))] + + [HarmonyPatch(typeof(HudManager), nameof(HudManager.SetHudActive), typeof(bool))] internal static class HudManagerSetHudActivePatch { public static void Prefix(HudManager __instance, [HarmonyArgument(0)] bool isActive) diff --git a/PeasAPI/CustomEndReason/CustomEndReason.cs b/PeasAPI/CustomEndReason/CustomEndReason.cs index a21edd1..0126373 100644 --- a/PeasAPI/CustomEndReason/CustomEndReason.cs +++ b/PeasAPI/CustomEndReason/CustomEndReason.cs @@ -1,8 +1,7 @@ using System.Collections.Generic; -using Hazel; using PeasAPI.CustomRpc; using PeasAPI.Roles; -using Reactor.Networking; +using Reactor.Networking.Rpc; using UnityEngine; namespace PeasAPI.CustomEndReason @@ -12,7 +11,7 @@ public class CustomEndReason /// /// Ends the game with the specified values /// - public CustomEndReason(Color color, string victoryText, string defeatText, string stinger, List winners) + public CustomEndReason(Color color, string victoryText, string defeatText, string stinger, List winners) { Rpc.Instance.Send(new RpcCustomEndReason.Data(color, victoryText, defeatText, stinger, winners)); } @@ -22,7 +21,7 @@ public CustomEndReason(Color color, string victoryText, string defeatText, strin /// public CustomEndReason(PlayerControl player) { - var role = player.GetRole(); + var role = player.GetCustomRole(); if (role == null) { @@ -37,7 +36,7 @@ public CustomEndReason(PlayerControl player) _winners.Add(_player.PlayerId); } - var winners = new List(); + var winners = new List(); foreach (var winner in _winners) { winners.Add(winner.GetPlayerInfo()); @@ -61,7 +60,7 @@ public CustomEndReason(PlayerControl player) _winners.Add(_player.PlayerId); } - var winners = new List(); + var winners = new List(); foreach (var winner in _winners) { winners.Add(winner.GetPlayerInfo()); @@ -108,12 +107,12 @@ public CustomEndReason(PlayerControl player) _winners.Add(player.PlayerId); foreach (var _player in GameData.Instance.AllPlayers) { - if (_player.PlayerId != player.PlayerId && _player.GetRole() == player.GetRole()) + if (_player.PlayerId != player.PlayerId && _player.GetCustomRole() == player.GetCustomRole()) _winners.Add(_player.PlayerId); } } - var winners = new List(); + var winners = new List(); foreach (var winner in _winners) { winners.Add(winner.GetPlayerInfo()); diff --git a/PeasAPI/CustomEndReason/EndReasonManager.cs b/PeasAPI/CustomEndReason/EndReasonManager.cs index 5a935f1..c671b3e 100644 --- a/PeasAPI/CustomEndReason/EndReasonManager.cs +++ b/PeasAPI/CustomEndReason/EndReasonManager.cs @@ -10,7 +10,7 @@ public class EndReasonManager public static Color Color; - public static List Winners; + public static List Winners; public static string VictoryText; @@ -37,13 +37,13 @@ private class SetEverythingUpPatch public static bool Prefix(EndGameManager __instance) { - if (TempData.EndReason != CustomGameOverReason) + if (EndGameResult.CachedGameOverReason != CustomGameOverReason) return true; - List _winners = new List(); + List _winners = new List(); foreach (var winner in Winners) { - _winners.Add(new WinningPlayerData(winner)); + _winners.Add(new CachedPlayerData(winner)); } __instance.DisconnectStinger = Stinger switch @@ -82,9 +82,9 @@ public static bool Prefix(EndGameManager __instance) transform.localScale = scaleVec; if (winner.IsDead) { - player.BodySprites.ToArray()[0].BodySprite.sprite = __instance.GhostSprite; + player.cosmetics.bodySprites.ToArray()[0].BodySprite.sprite = __instance.GhostSprite; player.SetDeadFlipX(i % 2 == 1); - player.HatSlot.color = GhostColor; + player.cosmetics.SetHatColor(GhostColor); } else { @@ -92,11 +92,11 @@ public static bool Prefix(EndGameManager __instance) player.SetSkin(winner.SkinId, winner.ColorId); } - PlayerControl.SetPlayerMaterialColors(winner.ColorId, player.CurrentBodySprite.BodySprite); - player.HatSlot.SetHat(winner.HatId, winner.ColorId); - PlayerControl.SetPetImage(winner.PetId, winner.ColorId, player.PetSlot); - player.NameText.text = winner.PlayerName; - player.NameText.transform.SetLocalZ(-15f); + PlayerMaterial.SetColors(winner.ColorId, player.cosmetics.currentBodySprite.BodySprite); + player.cosmetics.SetHat(winner.HatId, winner.ColorId); + //PlayerControl.SetPetImage(winner.PetId, winner.ColorId, player.PetSlot); + player.cosmetics.nameText.text = winner.PlayerName; + player.cosmetics.nameText.transform.SetLocalZ(-15f); } SoundManager.Instance.PlaySound(__instance.DisconnectStinger, false, 1f); @@ -110,7 +110,7 @@ private class AdjustEndScreenPatch { public static void Prefix(EndGameManager __instance) { - if (TempData.EndReason != CustomGameOverReason) + if (EndGameResult.CachedGameOverReason != CustomGameOverReason) return; __instance.DisconnectStinger = Stinger switch @@ -137,7 +137,7 @@ public static void Prefix(EndGameManager __instance) public static void Postfix(EndGameManager __instance) { - if (TempData.EndReason != CustomGameOverReason) + if (EndGameResult.CachedGameOverReason != CustomGameOverReason) return; Reset(); diff --git a/PeasAPI/CustomRpc/RpcCustomCheckColor.cs b/PeasAPI/CustomRpc/RpcCustomCheckColor.cs index 4cde6dc..ad5f34e 100644 --- a/PeasAPI/CustomRpc/RpcCustomCheckColor.cs +++ b/PeasAPI/CustomRpc/RpcCustomCheckColor.cs @@ -1,6 +1,6 @@ using Hazel; -using Reactor; -using Reactor.Networking; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; namespace PeasAPI.CustomRpc { diff --git a/PeasAPI/CustomRpc/RpcCustomEndReason.cs b/PeasAPI/CustomRpc/RpcCustomEndReason.cs index 98841f1..aa27497 100644 --- a/PeasAPI/CustomRpc/RpcCustomEndReason.cs +++ b/PeasAPI/CustomRpc/RpcCustomEndReason.cs @@ -2,8 +2,8 @@ using System.Linq; using Hazel; using PeasAPI.CustomEndReason; -using Reactor; -using Reactor.Networking; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; using UnityEngine; namespace PeasAPI.CustomRpc @@ -21,9 +21,9 @@ public readonly struct Data public readonly string VictoryText; public readonly string DefeatText; public readonly string Stinger; - public readonly List Winners; + public readonly List Winners; - public Data(Color color, string victoryText, string defeatText, string stinger, List winners) + public Data(Color color, string victoryText, string defeatText, string stinger, List winners) { Color = color; VictoryText = victoryText; @@ -66,7 +66,7 @@ public override Data Read(MessageReader reader) var winnerCount = reader.ReadInt32(); var _winners = reader.ReadBytes(winnerCount).ToList(); - var winners = new List(); + var winners = new List(); foreach (var winner in _winners) { winners.Add(winner.GetPlayerInfo()); @@ -88,7 +88,7 @@ public override void Handle(PlayerControl innerNetObject, Data data) EndReasonManager.Stinger = data.Stinger; if (AmongUsClient.Instance.AmHost) - ShipStatus.RpcEndGame(EndReasonManager.CustomGameOverReason, false); + GameManager.Instance.RpcEndGame(EndReasonManager.CustomGameOverReason, false); } } } \ No newline at end of file diff --git a/PeasAPI/CustomRpc/RpcInitializeRoles.cs b/PeasAPI/CustomRpc/RpcInitializeRoles.cs index a95001e..33b2390 100644 --- a/PeasAPI/CustomRpc/RpcInitializeRoles.cs +++ b/PeasAPI/CustomRpc/RpcInitializeRoles.cs @@ -4,9 +4,9 @@ using PeasAPI.CustomEndReason; using PeasAPI.GameModes; using PeasAPI.Roles; -using Reactor; -using Reactor.Extensions; -using Reactor.Networking; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; +using Reactor.Utilities.Extensions; namespace PeasAPI.CustomRpc { @@ -27,25 +27,25 @@ public override void Handle(PlayerControl innerNetObject) EndReasonManager.Reset(); - if (AmongUsClient.Instance.GameMode != global::GameModes.FreePlay) + if (AmongUsClient.Instance.NetworkMode != global::NetworkModes.FreePlay) { var rolesForPlayers = new List(); - var roles = Roles.RoleManager.Roles.Where(role => role.GetChance() == 100).ToList(); + var roles = Roles.RoleManager.Roles.Where(role => role.Chance == 100).ToList(); foreach (var role in roles) { - for (int i = 0; i < role.GetCount(); i++) + for (int i = 0; i < role.Count; i++) { rolesForPlayers.Add(role); } } var roles2 = (from role in Roles.RoleManager.Roles - where role.GetCount() > 0 && role.GetChance() > 0 && role.GetChance() < 100 + where role.Count > 0 && role.Chance > 0 && role.Chance < 100 select role).ToList(); foreach (var role in roles2) { - for (int i = 0; i < role.GetCount(); i++) + for (int i = 0; i < role.Count; i++) { rolesForPlayers.Add(role); } @@ -86,21 +86,21 @@ private void AssignRole(BaseRole role) { var nonRoleImpostors = Roles.RoleManager.Impostors.Where(id => id.GetPlayer().Data.Role.IsSimpleRole && - !RoleManager.IsGhostRole(id.GetPlayerInfo().Role.Role) && id.GetPlayer().GetRole() == null) + !RoleManager.IsGhostRole(id.GetPlayerInfo().Role.Role) && id.GetPlayer().GetCustomRole() == null) .ToArray(); if (nonRoleImpostors.Length == 0) return; if (Roles.RoleManager.HostMod.IsRole.ContainsKey(role) && Roles.RoleManager.HostMod.IsRole[role] && - PlayerControl.LocalPlayer.GetRole() == null) + PlayerControl.LocalPlayer.GetCustomRole() == null) { PlayerControl.LocalPlayer.RpcSetRole(role); return; } var chance = HashRandom.Next(101); - if (chance < role.GetChance()) + if (chance < role.Chance) { var member = nonRoleImpostors[PeasAPI.Random.Next(0, nonRoleImpostors.Length)]; @@ -111,21 +111,21 @@ private void AssignRole(BaseRole role) { var nonRoleCrewmates = Roles.RoleManager.Crewmates.Where(id => id.GetPlayer().Data.Role.IsSimpleRole && - !RoleManager.IsGhostRole(id.GetPlayerInfo().Role.Role) && id.GetPlayer().GetRole() == null) + !RoleManager.IsGhostRole(id.GetPlayerInfo().Role.Role) && id.GetPlayer().GetCustomRole() == null) .ToArray(); if (nonRoleCrewmates.Length == 0) return; if (Roles.RoleManager.HostMod.IsRole.ContainsKey(role) && Roles.RoleManager.HostMod.IsRole[role] && - PlayerControl.LocalPlayer.GetRole() == null) + PlayerControl.LocalPlayer.GetCustomRole() == null) { PlayerControl.LocalPlayer.RpcSetRole(role); return; } var chance = HashRandom.Next(101); - if (chance < role.GetChance()) + if (chance < role.Chance) { var member = nonRoleCrewmates[PeasAPI.Random.Next(0, nonRoleCrewmates.Length)]; diff --git a/PeasAPI/CustomRpc/RpcResetRoles.cs b/PeasAPI/CustomRpc/RpcResetRoles.cs index 9df2f3f..b1261bd 100644 --- a/PeasAPI/CustomRpc/RpcResetRoles.cs +++ b/PeasAPI/CustomRpc/RpcResetRoles.cs @@ -1,6 +1,5 @@ -using PeasAPI.Roles; -using Reactor; -using Reactor.Networking; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; namespace PeasAPI.CustomRpc { diff --git a/PeasAPI/CustomRpc/RpcSetColor.cs b/PeasAPI/CustomRpc/RpcSetColor.cs index 47bc526..08d1f6f 100644 --- a/PeasAPI/CustomRpc/RpcSetColor.cs +++ b/PeasAPI/CustomRpc/RpcSetColor.cs @@ -1,6 +1,6 @@ using Hazel; -using Reactor; -using Reactor.Networking; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; namespace PeasAPI.CustomRpc { diff --git a/PeasAPI/CustomRpc/RpcSetRole.cs b/PeasAPI/CustomRpc/RpcSetRole.cs index fb46441..d54e14a 100644 --- a/PeasAPI/CustomRpc/RpcSetRole.cs +++ b/PeasAPI/CustomRpc/RpcSetRole.cs @@ -1,7 +1,7 @@ using Hazel; using PeasAPI.Roles; -using Reactor; -using Reactor.Networking; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; namespace PeasAPI.CustomRpc { @@ -47,7 +47,7 @@ public override Data Read(MessageReader reader) public override void Handle(PlayerControl innerNetObject, Data data) { - data.Player.SetRole(data.Role); + data.Player.SetCustomRole(data.Role); } } } \ No newline at end of file diff --git a/PeasAPI/CustomRpc/RpcSetVanillaRole.cs b/PeasAPI/CustomRpc/RpcSetVanillaRole.cs index 3abbd68..7dd15e8 100644 --- a/PeasAPI/CustomRpc/RpcSetVanillaRole.cs +++ b/PeasAPI/CustomRpc/RpcSetVanillaRole.cs @@ -1,6 +1,7 @@ -using Hazel; -using Reactor; -using Reactor.Networking; +using AmongUs.GameOptions; +using Hazel; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; namespace PeasAPI.CustomRpc { diff --git a/PeasAPI/CustomRpc/RpcShowMessage.cs b/PeasAPI/CustomRpc/RpcShowMessage.cs index 59b785b..9b4208c 100644 --- a/PeasAPI/CustomRpc/RpcShowMessage.cs +++ b/PeasAPI/CustomRpc/RpcShowMessage.cs @@ -2,8 +2,8 @@ using HarmonyLib; using Hazel; using PeasAPI.Managers; -using Reactor; -using Reactor.Networking; +using Reactor.Networking.Attributes; +using Reactor.Networking.Rpc; namespace PeasAPI.CustomRpc { diff --git a/PeasAPI/CustomRpc/RpcUpdateSetting.cs b/PeasAPI/CustomRpc/RpcUpdateSetting.cs index 513dc9b..1304a17 100644 --- a/PeasAPI/CustomRpc/RpcUpdateSetting.cs +++ b/PeasAPI/CustomRpc/RpcUpdateSetting.cs @@ -1,72 +1,144 @@ -using Hazel; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using Hazel; using PeasAPI.Options; -using Reactor; -using Reactor.Networking; +using UnityEngine; namespace PeasAPI.CustomRpc { - [RegisterCustomRpc((uint) CustomRpcCalls.UpdateSetting)] - public class RpcUpdateSetting : PlayerCustomRpc + public static class RpcUpdateSetting { - public RpcUpdateSetting(PeasAPI plugin, uint id) : base(plugin, id) + public static IEnumerator SendRpc(CustomOption optionn = null, int RecipientId = -1) { - } + yield return new WaitForSecondsRealtime(0.5f); - public readonly struct Data - { - public readonly CustomOption Option; - public readonly object Value; + List options; + if (optionn != null) + options = new List { optionn }; + else + options = CustomOption.AllOptions; + + var writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, + (byte)CustomRpcCalls.UpdateSetting, SendOption.Reliable, RecipientId); - public Data(CustomOption option, object value) + foreach (var option in options) { - Option = option; - Value = value; - } - } + if (option.Type == CustomOptionType.Header) continue; - public override RpcLocalHandling LocalHandling => RpcLocalHandling.None; + if (writer.Position > 1000) + { + AmongUsClient.Instance.FinishRpcImmediately(writer); + writer = AmongUsClient.Instance.StartRpcImmediately(PlayerControl.LocalPlayer.NetId, + (byte)CustomRpcCalls.UpdateSetting, SendOption.Reliable, RecipientId); + } - public override void Write(MessageWriter writer, Data data) - { - writer.Write(data.Option.Id); - - if (data.Option.GetType() == typeof(CustomToggleOption)) - writer.Write((bool) data.Value); - else if (data.Option.GetType() == typeof(CustomNumberOption)) - writer.Write((float) data.Value); - else if (data.Option.GetType() == typeof(CustomStringOption)) - writer.Write((int) data.Value); - - //PeasApi.Logger.LogInfo("1: " + data.Option.Id + " " + data.Value); + writer.WritePacked(option.ID); + + switch (option.Type) + { + case CustomOptionType.Toggle: + writer.Write((bool)option.ValueObject); + break; + case CustomOptionType.Number: + { + switch (option.CustomRoleOptionType) + { + case CustomRoleOptionType.None: + switch ((option as CustomNumberOption).IntSafe) + { + case true: + writer.WritePacked((int)(float)option.ValueObject); + break; + case false: + writer.Write((float)option.ValueObject); + break; + } + + break; + case CustomRoleOptionType.Chance: + writer.Write(Convert.ToInt32(option.ValueObject)); + option.BaseRole.Chance = Convert.ToInt32(option.ValueObject); + break; + case CustomRoleOptionType.Count: + writer.Write(Convert.ToInt32(option.ValueObject)); + option.BaseRole.Count = + option.BaseRole.MaxCount = Convert.ToInt32(option.ValueObject); + break; + } + } + break; + case CustomOptionType.String: + writer.WritePacked((int)option.ValueObject); + break; + } + } + + AmongUsClient.Instance.FinishRpcImmediately(writer); } - public override Data Read(MessageReader reader) + public static void ReceiveRpc(MessageReader reader, bool AllOptions) { - //PeasApi.Logger.LogInfo("2"); - var id = reader.ReadString(); - //PeasApi.Logger.LogInfo("2b: " + id); - var option = OptionManager.CustomOptions.Find(_option => _option.Id == id); - object value = null; + PeasAPI.Logger.LogInfo( + $"Options received - {reader.BytesRemaining} bytes"); + while (reader.BytesRemaining > 0) + { + var id = reader.ReadPackedInt32(); + var customOption = + CustomOption.AllOptions.FirstOrDefault(option => + option.ID == id); // Works but may need to change to gameObject.name check + var type = customOption?.Type; + object value = null; + + switch (type) + { + case CustomOptionType.Toggle: + value = reader.ReadBoolean(); + break; + case CustomOptionType.Number: + switch ((customOption as CustomNumberOption).IntSafe) + { + case true: + value = (float)reader.ReadPackedInt32(); + break; + case false: + value = reader.ReadSingle(); + break; + } + + break; + case CustomOptionType.String: + value = reader.ReadPackedInt32(); + break; + } - if (option.GetType() == typeof(CustomToggleOption)) - value = reader.ReadBoolean(); - else if (option.GetType() == typeof(CustomNumberOption)) - value = reader.ReadSingle(); - else if (option.GetType() == typeof(CustomStringOption)) - value = reader.ReadInt32(); - - return new Data(option, value); + customOption?.Set(value, Notify: !AllOptions); + + if (LobbyInfoPane.Instance.LobbyViewSettingsPane.gameObject.activeSelf) + { + var panels = GameObject.FindObjectsOfType(); + foreach (var panel in panels) + if (panel.titleText.text == customOption.GetName() && + customOption.Type != CustomOptionType.Header) + panel.settingText.text = customOption.ToString(); + } + } } - public override void Handle(PlayerControl innerNetObject, Data data) + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.HandleRpc))] + public static class HandleRpc { - //PeasApi.Logger.LogInfo("4: " + data.Value.GetType()); - if (data.Option.GetType() == typeof(CustomToggleOption)) - ((CustomToggleOption) data.Option).SetValue((bool) data.Value); - else if (data.Option.GetType() == typeof(CustomNumberOption)) - ((CustomNumberOption) data.Option).SetValue((float) data.Value); - else if (data.Option.GetType() == typeof(CustomStringOption)) - ((CustomStringOption) data.Option).SetValue((int) data.Value); + private static void Postfix([HarmonyArgument(0)] byte callId, [HarmonyArgument(1)] MessageReader reader) + { + switch (callId) + { + case (byte)CustomRpcCalls.UpdateSetting: + ReceiveRpc(reader, reader.BytesRemaining > 8); + break; + } + } } } } \ No newline at end of file diff --git a/PeasAPI/Data.cs b/PeasAPI/Data.cs index 1331e56..6843ba5 100644 --- a/PeasAPI/Data.cs +++ b/PeasAPI/Data.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using Reactor.Extensions; +using System.Collections.Generic; using UnityEngine; namespace PeasAPI @@ -34,127 +30,5 @@ public CustomIntroScreen(bool overrideTeam = false, string team = null, string t RoleColor = roleColor.GetValueOrDefault(); } } - - public readonly struct Hat - { - public readonly string Name; - public readonly string ImagePath; - public readonly Assembly Assembly; - public readonly bool InFront; - public readonly bool NoBounce; - public readonly Vector2 ChipOffset; - public readonly Sprite BackImage; - public readonly Sprite FloorImage; - - public Hat(string name, string imagePath, Assembly assembly, bool inFront, bool noBounce, Vector2 chipOffset, Sprite backImage, Sprite floorImage) - { - Name = name; - ImagePath = imagePath; - Assembly = assembly; - InFront = inFront; - NoBounce = noBounce; - ChipOffset = chipOffset; - BackImage = backImage; - FloorImage = floorImage; - } - - public HatData CreateHat() - { - try - { - Texture2D tex = new Texture2D(128, 128, TextureFormat.ARGB32, false); - Stream myStream = Assembly.GetManifestResourceStream(ImagePath); - byte[] data = myStream.ReadFully(); - ImageConversion.LoadImage(tex, data, false); - - var newHat = ScriptableObject.CreateInstance(); - newHat.hatViewData.viewData = ScriptableObject.CreateInstance(); - newHat.hatViewData.viewData.MainImage = newHat.hatViewData.viewData.LeftMainImage = Sprite.Create( - tex, - new Rect(0, 0, tex.width, tex.height), - new Vector2(0.53f, 0.575f), - tex.width * 0.375f - ); - - newHat.ProductId = $"+{Name}"; - newHat.displayOrder += 100; - newHat.Free = true; - newHat.StoreName = Name; - newHat.name = Name; - - newHat.InFront = InFront; - newHat.NoBounce = NoBounce; - newHat.ChipOffset = ChipOffset; - newHat.hatViewData.viewData.BackImage = newHat.hatViewData.viewData.LeftBackImage = BackImage; - newHat.hatViewData.viewData.ClimbImage = newHat.hatViewData.viewData.LeftClimbImage = BackImage; - newHat.hatViewData.viewData.FloorImage = newHat.hatViewData.viewData.LeftFloorImage = FloorImage; - - return newHat; - } - catch (Exception e) - { - PeasAPI.Logger.LogError($"Error while creating a hat: {e}"); - } - - return null; - } - } - - public readonly struct Visor - { - public readonly string Name; - public readonly string ImagePath; - public readonly Assembly Assembly; - public readonly Vector2 ChipOffset; - public readonly Sprite ClimbImage; - public readonly Sprite FloorImage; - - public Visor(string name, string imagePath, Assembly assembly, Vector2 chipOffset, Sprite climbImage, Sprite floorImage) - { - Name = name; - ImagePath = imagePath; - Assembly = assembly; - ChipOffset = chipOffset; - ClimbImage = climbImage; - FloorImage = floorImage; - } - - public VisorData CreateVisor() - { - try - { - Texture2D tex = new Texture2D(128, 128, TextureFormat.ARGB32, false); - Stream myStream = Assembly.GetManifestResourceStream(ImagePath); - byte[] data = myStream.ReadFully(); - ImageConversion.LoadImage(tex, data, false); - - var newVisor = ScriptableObject.CreateInstance(); - newVisor.viewData.viewData = ScriptableObject.CreateInstance(); - newVisor.viewData.viewData.IdleFrame = newVisor.viewData.viewData.LeftIdleFrame = Sprite.Create( - tex, - new Rect(0, 0, tex.width, tex.height), - new Vector2(0.53f, 0.575f), - tex.width * 0.375f - ); - - newVisor.ProductId = $"+{Name}"; - newVisor.displayOrder += 100; - newVisor.Free = true; - newVisor.name = Name; - - newVisor.ChipOffset = ChipOffset; - newVisor.viewData.viewData.ClimbFrame = ClimbImage; - newVisor.viewData.viewData.FloorFrame = FloorImage; - - return newVisor; - } - catch (Exception e) - { - PeasAPI.Logger.LogError($"Error while creating a visor: {e.StackTrace}"); - } - - return null; - } - } } } \ No newline at end of file diff --git a/PeasAPI/Enums/FileType.cs b/PeasAPI/Enums/FileType.cs index 89b6fa3..a6ab859 100644 --- a/PeasAPI/Enums/FileType.cs +++ b/PeasAPI/Enums/FileType.cs @@ -1,6 +1,4 @@ -using System; - -namespace PeasAPI.Enums +namespace PeasAPI.Enums { public enum FileType { diff --git a/PeasAPI/Extensions.cs b/PeasAPI/Extensions.cs index 761240b..0a6188e 100644 --- a/PeasAPI/Extensions.cs +++ b/PeasAPI/Extensions.cs @@ -1,14 +1,11 @@ using System.Linq; -using HarmonyLib; +using AmongUs.GameOptions; using PeasAPI.CustomRpc; using PeasAPI.Options; using PeasAPI.Roles; -using Reactor.Extensions; -using Reactor.Networking; -using Reactor.Networking.MethodRpc; -using UnhollowerBaseLib; +using Reactor.Networking.Rpc; +using Reactor.Utilities.Extensions; using UnityEngine; -using Object = Il2CppSystem.Object; namespace PeasAPI { @@ -23,9 +20,9 @@ public static PlayerControl GetPlayer(this byte id) } /// - /// Gets a from it's id + /// Gets a from it's id /// - public static GameData.PlayerInfo GetPlayerInfo(this byte id) + public static NetworkedPlayerInfo GetPlayerInfo(this byte id) { return GameData.Instance.GetPlayerById(id); } @@ -39,12 +36,12 @@ public static void ToggleOutline(this PlayerControl player, bool active, Color c { if (active) { - player.MyRend.material.SetFloat("_Outline", 1f); - player.MyRend.material.SetColor("_OutlineColor", color); + player.cosmetics.currentBodySprite.BodySprite.material.SetFloat("_Outline", 1f); + player.cosmetics.currentBodySprite.BodySprite.material.SetColor("_OutlineColor", color); return; } - player.MyRend.material.SetFloat("_Outline", 0f); + player.cosmetics.currentBodySprite.BodySprite.material.SetFloat("_Outline", 0f); } public static Color SetAlpha(this Color original, float alpha) @@ -93,7 +90,7 @@ public static string GetTranslation(this StringNames stringName) /// /// Gets the role of a /// - public static BaseRole GetRole(this PlayerControl player) + public static BaseRole GetCustomRole(this PlayerControl player) { foreach (var _role in Roles.RoleManager.Roles) { @@ -103,11 +100,19 @@ public static BaseRole GetRole(this PlayerControl player) return null; } - + + public static bool IsCustomRole(this PlayerControl player) + { + if (player == null) + return false; + + return GetCustomRole(player) != null; + } + /// - /// Gets the role of a + /// Gets the role of a /// - public static BaseRole GetRole(this GameData.PlayerInfo player) + public static BaseRole GetCustomRole(this NetworkedPlayerInfo player) { foreach (var _role in Roles.RoleManager.Roles) { @@ -121,25 +126,25 @@ public static BaseRole GetRole(this GameData.PlayerInfo player) /// /// Checks if a is a certain role /// - public static bool IsRole(this PlayerControl player, BaseRole role) => player.GetRole() == role; + public static bool IsCustomRole(this PlayerControl player, BaseRole role) => player.GetCustomRole() == role; #nullable enable /// /// Gets the role of a /// - public static T? GetRole(this PlayerControl player) where T : BaseRole - => player.GetRole() as T; + public static T? GetCustomRole(this PlayerControl player) where T : BaseRole + => player.GetCustomRole() as T; /// /// Checks if a is a certain role /// - public static bool IsRole(this PlayerControl player) where T : BaseRole - => player.GetRole() != null; + public static bool IsCustomRole(this PlayerControl player) where T : BaseRole + => player.GetCustomRole() != null; /// /// Sets the role of a /// - public static void SetRole(this PlayerControl player, BaseRole? role) + public static void SetCustomRole(this PlayerControl player, BaseRole? role) { var oldRole = Roles.RoleManager.Roles.Where(r => r.Members.Contains(player.PlayerId)).ToList(); if (oldRole.Count != 0) @@ -160,8 +165,8 @@ public static void SetRole(this PlayerControl player, BaseRole? role) HudManager.Instance.KillButton.gameObject.SetActive(isImpostor && !isDead); HudManager.Instance.ImpostorVentButton.gameObject.SetActive(isImpostor && !isDead); - player.nameText.color = isImpostor ? Palette.ImpostorRed : Color.white; - player.nameText.text = player.name; + player.cosmetics.nameText.color = isImpostor ? Palette.ImpostorRed : Color.white; + player.cosmetics.nameText.text = player.name; } } @@ -177,7 +182,7 @@ public static void SetVanillaRole(this PlayerControl player, RoleTypes role) HudManager.Instance.MapButton.gameObject.SetActive(true); HudManager.Instance.ReportButton.gameObject.SetActive(true); HudManager.Instance.UseButton.gameObject.SetActive(true); - PlayerControl.LocalPlayer.RemainingEmergencies = PlayerControl.GameOptions.NumEmergencyMeetings; + PlayerControl.LocalPlayer.RemainingEmergencies = GameOptionsManager.Instance.currentNormalGameOptions.NumEmergencyMeetings; RoleManager.Instance.SetRole(player, role); player.Data.Role.SpawnTaskHeader(player); if (!DestroyableSingleton.InstanceExists) @@ -188,11 +193,11 @@ public static void SetVanillaRole(this PlayerControl player, RoleTypes role) { if (pc.Data.Role.TeamType == PlayerControl.LocalPlayer.Data.Role.TeamType) { - pc.nameText.color = pc.Data.Role.NameColor; + pc.cosmetics.nameText.color = pc.Data.Role.NameColor; } else { - pc.nameText.color = Palette.White; + pc.cosmetics.nameText.color = Palette.White; } }); } @@ -213,13 +218,13 @@ public static void RpcSetRole(this PlayerControl player, BaseRole? role) { Rpc.Instance.Send(new RpcSetRole.Data(player, role)); - player.SetRole(role); + player.SetCustomRole(role); } public static bool IsOnSameTeam(this PlayerControl player, PlayerControl otherPlayer) { - var role = player.GetRole(); - var otherRole = otherPlayer.GetRole(); + var role = player.GetCustomRole(); + var otherRole = otherPlayer.GetCustomRole(); if (role != null) { @@ -285,45 +290,7 @@ public static RoleTypes GetSimpleRoleType(this RoleTypes role) } #endregion Roles - - #region Options - - public static bool IsCustom(this OptionBehaviour option) - { - foreach (var customOption in OptionManager.CustomOptions) - { - if (customOption.Option == option) - return true; - } - - foreach (var customOption in OptionManager.CustomRoleOptions) - { - if (customOption.Option == option) - return true; - } - - return false; - } - - public static CustomOption? GetCustom(this OptionBehaviour option) - { - foreach (var customOption in OptionManager.CustomOptions) - { - if (customOption.Option == option) - return customOption; - } - - foreach (var customOption in OptionManager.CustomRoleOptions) - { - if (customOption.Option == option) - return customOption; - } - - return null; - } - - #endregion Options - + #region Position public static Vector3 SetX(this Transform transform, float x) { diff --git a/PeasAPI/GameModes/GameMode.cs b/PeasAPI/GameModes/GameMode.cs index ced0ca2..4887311 100644 --- a/PeasAPI/GameModes/GameMode.cs +++ b/PeasAPI/GameModes/GameMode.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; -using BepInEx.IL2CPP; +using AmongUs.GameOptions; +using BepInEx.Unity.IL2CPP; namespace PeasAPI.GameModes { @@ -35,7 +35,7 @@ public virtual bool CanKill(PlayerControl killer, PlayerControl victim) return true; } - public virtual bool OnMeetingCall(PlayerControl caller, GameData.PlayerInfo target) + public virtual bool OnMeetingCall(PlayerControl caller, NetworkedPlayerInfo target) { return true; } diff --git a/PeasAPI/GameModes/Patches.cs b/PeasAPI/GameModes/Patches.cs index 3f34ec8..54dbd61 100644 --- a/PeasAPI/GameModes/Patches.cs +++ b/PeasAPI/GameModes/Patches.cs @@ -1,8 +1,9 @@ using System; using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; using Il2CppSystem.Collections.Generic; -using Reactor; +using Reactor.Localization.Utilities; using UnityEngine; namespace PeasAPI.GameModes @@ -23,10 +24,10 @@ public static void Prefix(ShipStatus __instance) } } - [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.RpcEndGame))] - class ShipStatusRpcEndGamePatch + [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] + class GameManagerRpcEndGamePatch { - public static bool Prefix(ShipStatus __instance, [HarmonyArgument(0)] GameOverReason reason) + public static bool Prefix(GameManager __instance, [HarmonyArgument(0)] GameOverReason reason) { foreach (var mode in GameModeManager.Modes) { @@ -101,7 +102,7 @@ public static void AllowVanillaRolesPatch(PlayerControl __instance, [HarmonyArgu } } - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.SetRole))] + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CoSetRole))] [HarmonyPrefix] public static void AssignLocalRolePatch(PlayerControl __instance, [HarmonyArgument(0)] ref RoleTypes roleType) { @@ -125,16 +126,18 @@ public static bool Prefix(SabotageButton __instance) { if (mode.Enabled) { - HudManager.Instance.ShowMap((Action) (map => + var mapOptions = new MapOptions { - foreach (MapRoom mapRoom in map.infectedOverlay.rooms.ToArray()) - { - mapRoom.gameObject.SetActive(mode.AllowSabotage(mapRoom.room)); - } - - map.ShowSabotageMap(); - })); + Mode = MapOptions.Modes.Sabotage + }; + MapBehaviour.Instance.Show(mapOptions); + + foreach (MapRoom mapRoom in MapBehaviour.Instance.infectedOverlay.rooms.ToArray()) + { + mapRoom.gameObject.SetActive(mode.AllowSabotage(mapRoom.room)); + } + return false; } } @@ -155,13 +158,10 @@ public static void Prefix(MapBehaviour __instance) { if (mode.Enabled) { - HudManager.Instance.ShowMap((Action) (map => + foreach (MapRoom mapRoom in __instance.infectedOverlay.rooms.ToArray()) { - foreach (MapRoom mapRoom in map.infectedOverlay.rooms.ToArray()) - { - mapRoom.gameObject.SetActive(mode.AllowSabotage(mapRoom.room)); - } - })); + mapRoom.gameObject.SetActive(mode.AllowSabotage(mapRoom.room)); + } } } } @@ -171,7 +171,7 @@ public static void Prefix(MapBehaviour __instance) [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CmdReportDeadBody))] class PlayerControlCmdReportDeadBodyPatch { - public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] GameData.PlayerInfo target) + public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] NetworkedPlayerInfo target) { foreach (var mode in GameModeManager.Modes) { @@ -183,10 +183,10 @@ public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] GameDat } } - [HarmonyPatch(typeof(PlayerControl._CoSetTasks_d__112), nameof(PlayerControl._CoSetTasks_d__112.MoveNext))] + [HarmonyPatch(typeof(PlayerControl._CoSetTasks_d__103), nameof(PlayerControl._CoSetTasks_d__103.MoveNext))] public static class PlayerControlSetTasks { - public static void Postfix(PlayerControl._CoSetTasks_d__112 __instance) + public static void Postfix(PlayerControl._CoSetTasks_d__103 __instance) { if (__instance == null) return; @@ -292,7 +292,11 @@ public static void RoleTeamPatch(IntroCutscene __instance, [HarmonyArgument(0)] [HarmonyPostfix] static void SetupGameModeSetting(AmongUsClient __instance) { - GameModeManager.GameModeOption.Values = GameModeManager.Modes.ConvertAll(mode => mode.Name).Prepend("None").ToList().ConvertAll(mode => (StringNames) CustomStringName.Register(mode)); + var modeNames = GameModeManager.Modes.ConvertAll(mode => mode.Name); + + modeNames.Insert(0, "None"); + + GameModeManager.GameModeOption.Values = modeNames.ToArray(); } } } \ No newline at end of file diff --git a/PeasAPI/Managers/CustomColorManager.cs b/PeasAPI/Managers/CustomColorManager.cs index cdbcd36..582329c 100644 --- a/PeasAPI/Managers/CustomColorManager.cs +++ b/PeasAPI/Managers/CustomColorManager.cs @@ -1,14 +1,10 @@ -using System; using System.Collections.Generic; using System.Linq; +using AmongUs.Data.Legacy; using BepInEx.Configuration; using HarmonyLib; -using InnerNet; -using PeasAPI.CustomRpc; -using Reactor; -using Reactor.Extensions; -using Reactor.Networking; -using UnhollowerBaseLib; +using Reactor.Localization.Utilities; +using Reactor.Utilities.Extensions; using UnityEngine; using Object = UnityEngine.Object; @@ -61,7 +57,7 @@ public AUColor(Color body, Color shadow, string name) { Body = body; Shadow = shadow; - Name = CustomStringName.Register(name); + Name = CustomStringName.CreateAndRegister(name); } } @@ -138,7 +134,7 @@ private static void OnEnablePostfix(PlayerTab __instance) [HarmonyPatch(nameof(PlayerTab.SelectColor))] private static void SelectColor(PlayerTab __instance, int colorId) { - __instance.PlayerPreview.HatSlot.SetHat(SaveManager.LastHat, colorId); + __instance.PlayerPreview.cosmetics.SetHat(LegacySaveManager.LastHat, colorId); } } @@ -188,7 +184,7 @@ private static bool CheckColor(byte bodyColor, PlayerControl __instance) __instance.RpcSetColor(bodyColor); return false; - bool ColorIsOccupied(GameData.PlayerInfo p) + bool ColorIsOccupied(NetworkedPlayerInfo p) { return !p.Disconnected && p.PlayerId != __instance.PlayerId && p.DefaultOutfit.ColorId == bodyColor; @@ -196,24 +192,23 @@ bool ColorIsOccupied(GameData.PlayerInfo p) } } - // Prevent custom color from being saved inside SaveManager - [HarmonyPatch(typeof(SaveManager), nameof(SaveManager.BodyColor))] - private static class SaveManagerPatch + // Prevent custom color from being saved inside LegacySaveManager + private static class LegacySaveManagerPatch { private const byte MAXColor = 17; private static ConfigEntry Data => PeasAPI.ConfigFile - .Bind("CustomSaveManager", "Player Color ID", (byte) SaveManager.colorConfig); + .Bind("CustomLegacySaveManager", "Player Color ID", (byte) LegacySaveManager.colorConfig); + [HarmonyPatch(typeof(LegacySaveManager), nameof(LegacySaveManager.BodyColor), MethodType.Getter)] [HarmonyPrefix] - [HarmonyPatch(MethodType.Getter)] private static bool GetterPatch(ref byte __result) { __result = Data.Value; return false; } + [HarmonyPatch(typeof(LegacySaveManager), nameof(LegacySaveManager.BodyColor), MethodType.Setter)] [HarmonyPrefix] - [HarmonyPatch(MethodType.Setter)] private static bool SetterPatch(byte value) { Data.Value = value; diff --git a/PeasAPI/Managers/CustomHatManager.cs b/PeasAPI/Managers/CustomHatManager.cs deleted file mode 100644 index e6a46d4..0000000 --- a/PeasAPI/Managers/CustomHatManager.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using HarmonyLib; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using UnityEngine; -using static PeasAPI.Data; - -namespace PeasAPI.Managers -{ - public class CustomHatManager - { - public static List CustomHats = new List(); - public static List CustomVisors = new List(); - - public static void RegisterNewHat(string name, string imagePath, Vector2 chipOffset = new Vector2(), bool inFront = true, bool noBounce = true, Sprite backImage = null, Sprite floorImage = null) - { - var hat = new Hat(name, imagePath, Assembly.GetCallingAssembly(), inFront, noBounce, chipOffset, backImage, floorImage); - - CustomHats.Add(hat); - - if (PeasAPI.Logging) - PeasAPI.Logger.LogInfo($"Registered hat {name} from {Assembly.GetCallingAssembly().GetName().Name}"); - } - - public static void RegisterNewVisor(string name, string imagePath, Vector2 chipOffset = new Vector2(), Sprite climbImage = null, Sprite floorImage = null) - { - var visor = new Visor(name, imagePath, Assembly.GetCallingAssembly(), chipOffset, climbImage, floorImage); - - CustomVisors.Add(visor); - - if (PeasAPI.Logging) - PeasAPI.Logger.LogInfo($"Registered visor {name} from {Assembly.GetCallingAssembly().GetName().Name}"); - } - } - - [HarmonyPatch(typeof(HatManager), nameof(HatManager.GetHatById))] - public static class HatManagerPatch - { - private static bool modded = false; - - public static void Prefix(HatManager __instance) - { - if (modded) - return; - - modded = true; - - foreach (var hat in CustomHatManager.CustomHats) - __instance.allHats.Add(hat.CreateHat()); - foreach (var visor in CustomHatManager.CustomVisors) - __instance.allVisors.Add(visor.CreateVisor()); - - __instance.allHats.ToArray().ToList().Sort((h1, h2) => String.Compare(h2.ProductId, h1.ProductId, StringComparison.Ordinal)); - - __instance.allVisors.ToArray().ToList().Sort((h1, h2) => String.Compare(h2.ProductId, h1.ProductId, StringComparison.Ordinal)); - } - } -} \ No newline at end of file diff --git a/PeasAPI/Managers/CustomServerManager.cs b/PeasAPI/Managers/CustomServerManager.cs index 245a8be..9ada637 100644 --- a/PeasAPI/Managers/CustomServerManager.cs +++ b/PeasAPI/Managers/CustomServerManager.cs @@ -2,41 +2,26 @@ using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using AmongUs.Data.Player; using HarmonyLib; +using UnityEngine; +using UnityEngine.Events; namespace PeasAPI.Managers { public class CustomServerManager { - public static List CustomServer = new List(); + public static List CustomServer = new(); /// /// Adds a custom region to the game /// public static void RegisterServer(string name, string ip, ushort port) { - if (Uri.CheckHostName(ip).ToString() == "Dns") - { - try - { - foreach (IPAddress address in Dns.GetHostAddresses(ip)) - if (address.AddressFamily == AddressFamily.InterNetwork) - { - ip = address.ToString(); - break; - } - } - catch - { - } - } - - CustomServer.Add(new DnsRegionInfo(ip, name, StringNames.NoTranslation, ip, port, false) - .Cast()); - ServerManager.Instance.AddOrUpdateRegion(new DnsRegionInfo(ip, name, StringNames.NoTranslation, ip, port, false) - .Cast()); + CustomServer.Add(new StaticHttpRegionInfo(name, StringNames.NoTranslation, ip, + new[] { new ServerInfo(name + "-1", ip, port, false) })); } - + //Skidded from https://github.com/edqx/Edward.SkipAuth [HarmonyPatch(typeof(AuthManager._CoConnect_d__4), nameof(AuthManager._CoConnect_d__4.MoveNext))] public static class DoNothingInConnect @@ -64,7 +49,7 @@ public static void Postfix(ServerManager __instance) var defaultRegions = new List(); foreach (var server in CustomServer) { - defaultRegions.Add(server); + defaultRegions.Add(server.Cast()); } ServerManager.DefaultRegions = defaultRegions.ToArray(); __instance.AvailableRegions = defaultRegions.ToArray(); @@ -80,19 +65,66 @@ public static void Postfix(MainMenuManager __instance) { if (!_initialized && CustomServer.Count != 0 && ServerManager.Instance.CurrentRegion.Name != CustomServer[0].Name) { - ServerManager.Instance.SetRegion(CustomServer[0]); + ServerManager.Instance.SetRegion(CustomServer[0].Cast()); } _initialized = true; } } - [HarmonyPatch(typeof(StatsManager), nameof(StatsManager.AmBanned), MethodType.Getter)] - public static class AmBannedPatch + [HarmonyPatch(typeof(PlayerBanData), nameof(PlayerBanData.IsBanned), MethodType.Getter)] + public static class IsBannedPatch { public static void Postfix(out bool __result) { __result = false; } } + + [HarmonyPatch(typeof(ServerDropdown), nameof(ServerDropdown.FillServerOptions))] + public static class ServerDropdownPatch + { + public static bool Prefix(ServerDropdown __instance) + { + var num = 0; + __instance.background.size = new Vector2(8.4f, 4.8f); + + foreach (var regionInfo in DestroyableSingleton.Instance.AvailableRegions) + { + if (DestroyableSingleton.Instance.CurrentRegion.Equals(regionInfo)) + { + __instance.defaultButtonSelected = __instance.firstOption; + __instance.firstOption.ChangeButtonText( + DestroyableSingleton.Instance.GetStringWithDefault( + regionInfo.TranslateName, + regionInfo.Name)); + } + else + { + var region = regionInfo; + var serverListButton = __instance.ButtonPool.Get(); + var x = num % 2 == 0 ? -2 : 2; + var y = -0.55f * (num / 2); + serverListButton.transform.localPosition = new Vector3(x, __instance.y_posButton + y, -1f); + serverListButton.transform.localScale = Vector3.one; + serverListButton.Text.text = + DestroyableSingleton.Instance.GetStringWithDefault( + regionInfo.TranslateName, + regionInfo.Name); + serverListButton.Text.ForceMeshUpdate(); + serverListButton.Button.OnClick.RemoveAllListeners(); + serverListButton.Button.OnClick.AddListener((UnityAction)(() => { __instance.ChooseOption(region); })); + __instance.controllerSelectable.Add(serverListButton.Button); + __instance.background.transform.localPosition = new Vector3( + 0f, + __instance.initialYPos + (-0.3f * (num / 2)), + 0f); + __instance.background.size = new Vector2(__instance.background.size.x, 1.2f + (0.6f * (num / 2))); + num++; + } + } + + return false; + } + } } } \ No newline at end of file diff --git a/PeasAPI/Managers/PlayerMenuManager.cs b/PeasAPI/Managers/PlayerMenuManager.cs index eaf3623..b23e736 100644 --- a/PeasAPI/Managers/PlayerMenuManager.cs +++ b/PeasAPI/Managers/PlayerMenuManager.cs @@ -3,8 +3,7 @@ using System.Collections.Generic; using System.Linq; using HarmonyLib; -using Newtonsoft.Json.Utilities; -using Reactor; +using Reactor.Utilities; using TMPro; using UnityEngine; using UnityEngine.Events; @@ -41,7 +40,7 @@ private static IEnumerator CoCreatePlayerMenu(List players, Action().Locked = false; @@ -121,7 +120,7 @@ private static void CloseMenu() [HarmonyPatch] internal static class Patches { - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CoStartMeeting))] + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.StartMeeting))] [HarmonyPrefix] public static void OnMeetingStartPatch(PlayerControl __instance) { @@ -137,7 +136,7 @@ public static void MeetingHudOnStartPatch(MeetingHud __instance) { if (IsMenuOpen) { - HudManager.Instance.Chat.SetPosition(null); + HudManager.Instance.Chat.chatButtonAspectPosition = null; HudManager.Instance.Chat.SetVisible(false); __instance.discussionTimer = 20; } @@ -185,7 +184,7 @@ public static bool DisableDeadOverlayPatch(MeetingHud __instance) [HarmonyPrefix] public static bool DummyDontVotePatch(DummyBehaviour __instance) { - GameData.PlayerInfo data = __instance.myPlayer.Data; + NetworkedPlayerInfo data = __instance.myPlayer.Data; if (data == null || data.IsDead) { return false; diff --git a/PeasAPI/Managers/TextMessageManager.cs b/PeasAPI/Managers/TextMessageManager.cs index fe3992d..6094743 100644 --- a/PeasAPI/Managers/TextMessageManager.cs +++ b/PeasAPI/Managers/TextMessageManager.cs @@ -1,10 +1,11 @@ using System.Collections; using System.Collections.Generic; -using BepInEx.IL2CPP.Utils.Collections; +using BepInEx.Unity.IL2CPP.Utils.Collections; using HarmonyLib; using PeasAPI.CustomRpc; -using Reactor.Extensions; -using Reactor.Networking; +using Reactor.Networking.Rpc; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; @@ -16,7 +17,7 @@ public static class TextMessageManager public static void ShowMessage(string message, float duration) { - Reactor.Coroutines.Start(CoShowText(message, duration)); + Coroutines.Start(CoShowText(message, duration)); } public static void RpcShowMessage(string message, float duration, List targets) diff --git a/PeasAPI/Managers/UpdateManager.cs b/PeasAPI/Managers/UpdateManager.cs index ef01d58..41561d9 100644 --- a/PeasAPI/Managers/UpdateManager.cs +++ b/PeasAPI/Managers/UpdateManager.cs @@ -5,8 +5,7 @@ using System.Text; using PeasAPI.Enums; using PeasAPI.Managers.UpdateTools; -using Reactor; -using Reactor.Extensions; +using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using Object = UnityEngine.Object; diff --git a/PeasAPI/Managers/UpdateTools/GitHubUpdater.cs b/PeasAPI/Managers/UpdateTools/GitHubUpdater.cs index 82f42e4..72bfdb6 100644 --- a/PeasAPI/Managers/UpdateTools/GitHubUpdater.cs +++ b/PeasAPI/Managers/UpdateTools/GitHubUpdater.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Net.Http.Headers; using System.Reflection; using System.Text.Json; using PeasAPI.Enums; diff --git a/PeasAPI/Managers/UpdateTools/UpdateListener.cs b/PeasAPI/Managers/UpdateTools/UpdateListener.cs index 636d2bf..8b24874 100644 --- a/PeasAPI/Managers/UpdateTools/UpdateListener.cs +++ b/PeasAPI/Managers/UpdateTools/UpdateListener.cs @@ -2,13 +2,11 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Net; using System.Net.Http; using System.Reflection; using System.Text.Json; -using BepInEx.IL2CPP; +using BepInEx.Unity.IL2CPP; using PeasAPI.Enums; -using Reactor; using UnityEngine; namespace PeasAPI.Managers.UpdateTools @@ -103,29 +101,32 @@ private string GetAssemblyPath() public virtual void UpdateMod() { - using var webClient = new WebClient(); - var directoryName = Path.GetDirectoryName(Application.dataPath); var assemblyPath = GetAssemblyPath(); var text = $"OutdatedMods\\{Name}.dll"; - + Directory.CreateDirectory($"{directoryName}\\OutdatedMods"); if (File.Exists(text)) File.Delete(text); File.Move(assemblyPath, text); - + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.Add("User-Agent", "PeasAPI Updater"); + switch (Type) { case FileType.Dll: - webClient.DownloadFile(AssetLink, $"{Name}.dll"); + var dllBytes = httpClient.GetByteArrayAsync(AssetLink).Result; + File.WriteAllBytes($"{Name}.dll", dllBytes); File.Move($"{Name}.dll", assemblyPath); break; - + case FileType.Zip: - webClient.DownloadFile(AssetLink, $"{Name}.zip"); + var zipBytes = httpClient.GetByteArrayAsync(AssetLink).Result; + File.WriteAllBytes($"{Name}.zip", zipBytes); ZipFile.ExtractToDirectory($"{Name}.zip", "BepInEx\\plugins", true); File.Delete($"{Name}.zip"); break; - + case FileType.First: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(); } diff --git a/PeasAPI/Managers/WatermarkManager.cs b/PeasAPI/Managers/WatermarkManager.cs index db3e7bc..3c70cfc 100644 --- a/PeasAPI/Managers/WatermarkManager.cs +++ b/PeasAPI/Managers/WatermarkManager.cs @@ -1,9 +1,13 @@ using System.Collections.Generic; using BepInEx; using HarmonyLib; +using Il2CppInterop.Runtime; +using InnerNet; using Reactor; -using UnhollowerRuntimeLib; +using Reactor.Patches; +using TMPro; using UnityEngine; +using static PeasAPI.Managers.WatermarkManager; namespace PeasAPI.Managers { @@ -16,76 +20,62 @@ public class Watermark /// public string VersionText { get; set; } - /// - /// How much the version text should be lowered - /// - public Vector3 VersionTextOffset { get; set; } - /// /// Text that gets added to the ping text /// public string PingText { get; set; } - /// - /// How much the ping text should be lowered - /// - public Vector3 PingTextOffset { get; set; } - - public Watermark(string versionText, string pingText, - Vector3 versionTextOffset, Vector3 pingTextOffset) + public Watermark(string versionText, string pingText) { VersionText = versionText; PingText = pingText; - VersionTextOffset = versionTextOffset; - PingTextOffset = pingTextOffset; } } private static List Watermarks = new List(); - public static readonly Vector2 defaultVersionTextOffset = new (0f, -0.2f); - public static readonly Vector2 defaultPingTextOffset = new Vector2(0f, 0f); - - public static Watermark PeasApiWatermark = new Watermark($"\nPeasAPI {PeasAPI.Version} by Peasplayer\nReactor v{ReactorPlugin.Version}\nBepInEx v{Paths.BepInExVersion}", - "\nPeasAPI", new Vector2(), new Vector2()); + public static Watermark PeasApiWatermark = new Watermark($"PeasAPI {PeasAPI.Version} by Peasplayer", + "\nPeasAPI"); - public static void AddWatermark(string versionText, string pingText, - Vector2 versionTextOffset = new Vector2(), Vector2 pingTextOffset = new Vector2()) + public static void AddWatermark(string versionText, string pingText) { - var watermark = new Watermark(versionText, pingText, versionTextOffset, pingTextOffset); + var watermark = new Watermark(versionText, pingText); Watermarks.Add(watermark); } - [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] - public static class VersionShowerStartPatch + static bool haveStart = false; + + [HarmonyPatch(typeof(MainMenuManager), nameof(MainMenuManager.Start))] + public static class MainMenuManagerStartPatch { - static void Postfix(VersionShower __instance) + static void Postfix(MainMenuManager __instance) { - foreach (var watermark in Watermarks) + if (!haveStart) { - __instance.transform.position += watermark.VersionTextOffset; - - if (watermark.VersionText != null) - __instance.text.text += watermark.VersionText; - - foreach (var gameObject in Object.FindObjectsOfTypeAll(Il2CppType.Of())) - if (gameObject.name.Contains("ReactorVersion")) - Object.Destroy(gameObject); - } - - if (PeasAPI.ShamelessPlug) - { - __instance.transform.position += PeasApiWatermark.VersionTextOffset; + haveStart = true; - if (PeasApiWatermark.VersionText != null) - __instance.text.text += PeasApiWatermark.VersionText; - - foreach (var gameObject in Object.FindObjectsOfTypeAll(Il2CppType.Of())) - if (gameObject.name.Contains("ReactorVersion")) - Object.Destroy(gameObject); + foreach (var watermark in Watermarks) + { + if (watermark.VersionText != null) + { + ReactorVersionShower.TextUpdated += text => + { + text.text += "\n" + watermark.VersionText; + }; + } + } + + if (PeasAPI.ShamelessPlug) + { + if (PeasApiWatermark.VersionText != null) + { + ReactorVersionShower.TextUpdated += text => + { + text.text += "\n" + PeasApiWatermark.VersionText; + }; + } + } } - - __instance.transform.position = new Vector3(-5.2333f, 2.85f, 0f) - new Vector3(0f, 0.2875f / 2 * (__instance.text.text.Split('\n').Length - 1)); } } @@ -94,19 +84,27 @@ public static class PingTrackerUpdatePatch { public static void Postfix(PingTracker __instance) { - __instance.transform.localPosition = new Vector3(2.5833f, 2.9f, 0f); - foreach (var watermark in Watermarks) + var position = __instance.GetComponent(); + if (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started) { - __instance.transform.localPosition += watermark.PingTextOffset; - + __instance.text.alignment = TextAlignmentOptions.Top; + position.Alignment = AspectPosition.EdgeAlignments.Top; + position.DistanceFromEdge = new Vector3(1.5f, 0.11f, 0); + } + else + { + position.Alignment = AspectPosition.EdgeAlignments.LeftTop; + __instance.text.alignment = TextAlignmentOptions.TopLeft; + position.DistanceFromEdge = new Vector3(0.5f, 0.11f); + } + foreach (var watermark in Watermarks) + { if (watermark.PingText != null) __instance.text.text += watermark.PingText; } if (PeasAPI.ShamelessPlug) { - __instance.transform.localPosition += PeasApiWatermark.PingTextOffset; - if (PeasApiWatermark.PingText != null) __instance.text.text += PeasApiWatermark.PingText; } diff --git a/PeasAPI/Options/CustomHeaderOption.cs b/PeasAPI/Options/CustomHeaderOption.cs new file mode 100644 index 0000000..f367c3f --- /dev/null +++ b/PeasAPI/Options/CustomHeaderOption.cs @@ -0,0 +1,15 @@ +namespace PeasAPI.Options; + +public class CustomHeaderOption : CustomOption +{ + public CustomHeaderOption(MultiMenu menu, string name) : base(num++, menu, name, + CustomOptionType.Header, 0) + { + } + + public override void OptionCreated() + { + base.OptionCreated(); + Setting.Cast().TitleText.text = GetName(); + } +} \ No newline at end of file diff --git a/PeasAPI/Options/CustomNumberOption.cs b/PeasAPI/Options/CustomNumberOption.cs index ff3be6f..c0a6775 100644 --- a/PeasAPI/Options/CustomNumberOption.cs +++ b/PeasAPI/Options/CustomNumberOption.cs @@ -1,113 +1,59 @@ -using System; -using System.Reflection; -using BepInEx.Configuration; -using PeasAPI.CustomRpc; -using Reactor; -using Reactor.Networking; -using Object = UnityEngine.Object; +using System; +using PeasAPI.Roles; +using UnityEngine; -namespace PeasAPI.Options +namespace PeasAPI.Options; + +public class CustomNumberOption : CustomOption { - public class CustomNumberOption : CustomOption + public CustomNumberOption(MultiMenu multiMenu, string optionName, float value, + float increment, float min, float max, + Func format = null, CustomRoleOptionType customRoleOptionType = CustomRoleOptionType.None, + BaseRole baseRole = null) + : base(num++, multiMenu, optionName, CustomOptionType.Number, value, format, customRoleOptionType, + baseRole) { - public float Value { get; private set; } - - public float OldValue { get; private set; } - - public float MinValue { get; set; } - - public float MaxValue { get; set; } - - public float Increment { get; set; } - - public NumberSuffixes SuffixType { get; set; } - - public delegate void OnValueChangedHandler(CustomNumberOptionValueChangedArgs args); - - public event OnValueChangedHandler OnValueChanged; - - private ConfigEntry _configEntry; - - public class CustomNumberOptionValueChangedArgs - { - public CustomNumberOption Option; - - public float OldValue; - - public float NewValue; - - public CustomNumberOptionValueChangedArgs(CustomNumberOption option, float oldValue, float newValue) - { - Option = option; - OldValue = oldValue; - NewValue = newValue; - } - } + Min = min; + Max = max; + Increment = increment; + IntSafe = Min % 1 == 0 && Max % 1 == 0 && Increment % 1 == 0; + } - public void SetValue(float value) - { - var oldValue = Value; - - if (AmongUsClient.Instance.AmHost && _configEntry != null) - _configEntry.Value = value; - - Value = value; - OldValue = oldValue; - - ValueChanged(value, oldValue); + protected float Min { get; set; } + protected float Max { get; set; } + protected float Increment { get; set; } + public bool IntSafe { get; private set; } - if (AmongUsClient.Instance.AmHost) - Rpc.Instance.Send(new RpcUpdateSetting.Data(this, value)); - } - - public void ValueChanged(float newValue, float oldValue) - { - var args = new CustomNumberOptionValueChangedArgs(this, oldValue, newValue); - OnValueChanged?.Invoke(args); - } + public float Value => (float)ValueObject; - internal OptionBehaviour CreateOption(NumberOption numberOptionPrefab) - { - NumberOption numberOption = - Object.Instantiate(numberOptionPrefab, numberOptionPrefab.transform.parent); - - numberOption.TitleText.text = Title; - numberOption.Title = CustomStringName.Register(Title); - numberOption.Value = Value; - numberOption.ValidRange = new FloatRange(MinValue, MaxValue); - numberOption.Increment = Increment; - numberOption.SuffixType = SuffixType; + public void Increase() + { + var increment = Increment > 5 && Input.GetKeyInt(KeyCode.LeftShift) ? 5 : Increment; - Option = numberOption; + if (Value + increment > + Max + 0.001f) // the slight increase is because of the stupid float rounding errors in the Giant speed + Set(Min); + else + Set(Value + increment); + } - numberOption.OnValueChanged = new Action(behaviour => - { - SetValue(numberOption.Value); - }); + public void Decrease() + { + var increment = Increment > 5 && Input.GetKeyInt(KeyCode.LeftShift) ? 5 : Increment; - return numberOption; - } - - public CustomNumberOption(string id, string title, float minValue, float maxValue, float increment, float defaultValue, NumberSuffixes suffixType) : base(title) - { - Id = $"{Assembly.GetCallingAssembly().GetName().Name}.NumberOption.{id}"; - try - { - _configEntry = PeasAPI.ConfigFile.Bind("Options", Id, defaultValue); - } - catch (Exception e) - { - PeasAPI.Logger.LogError($"Error while loading the option \"{title}\": {e.Source}"); - } + if (Value - increment < Min - 0.001f) // added it here to in case I missed something else + Set(Max); + else + Set(Value - increment); + } - Value = _configEntry?.Value ?? defaultValue; - MinValue = minValue; - MaxValue = maxValue; - Increment = increment; - SuffixType = suffixType; - HudFormat = "{0}: {1}{2}"; - - OptionManager.CustomOptions.Add(this); - } + public override void OptionCreated() + { + base.OptionCreated(); + var number = Setting.Cast(); + number.ValidRange = new FloatRange(Min, Max); + number.Increment = Increment; + number.Value = number.oldValue = Value; + number.ValueText.text = ToString(); } } \ No newline at end of file diff --git a/PeasAPI/Options/CustomOption.cs b/PeasAPI/Options/CustomOption.cs index af0fd33..2cd467d 100644 --- a/PeasAPI/Options/CustomOption.cs +++ b/PeasAPI/Options/CustomOption.cs @@ -1,26 +1,115 @@ -namespace PeasAPI.Options +using System; +using System.Collections.Generic; +using PeasAPI.CustomRpc; +using PeasAPI.Roles; +using Reactor.Localization.Utilities; +using Reactor.Utilities; + +namespace PeasAPI.Options; + +public class CustomOption { - public abstract class CustomOption + public static List AllOptions = new(); + + public static int num = 1; + public readonly int ID; + public readonly MultiMenu Menu; + + public BaseRole BaseRole; + public CustomRoleOptionType CustomRoleOptionType; + + public Func Format; + public string Name; + public bool IsRoleOption; + + public StringNames StringName; + + public CustomOption(int id, MultiMenu menu, string name, CustomOptionType type, + object defaultValue, + Func format = null, CustomRoleOptionType customRoleOptionType = CustomRoleOptionType.None, + BaseRole baseRole = null, bool isRoleOption = false) { - public string Title { get; set; } - - public string Id { get; internal set; } + ID = id; + Menu = menu; + Name = name; + Type = type; + DefaultValue = ValueObject = defaultValue; + Format = format ?? (obj => $"{obj}"); + BaseRole = baseRole; + CustomRoleOptionType = customRoleOptionType; - public bool HudVisible { get; set; } = true; - - public bool MenuVisible { get; set; } = true; - - public bool AdvancedRoleOption { get; set; } + if (Type == CustomOptionType.Button) return; + AllOptions.Add(this); + Set(ValueObject); - public string HudFormat { get; set; } = "{0}"; + StringName = CustomStringName.CreateAndRegister( + customRoleOptionType == CustomRoleOptionType.Chance || customRoleOptionType == CustomRoleOptionType.Count + ? Utility.ColorString(baseRole.Color, baseRole.Name) + $" {GetName()}" + : GetName()); - internal bool IsFromPeasAPI { get; set; } = false; - - public OptionBehaviour Option { get; internal set; } + IsRoleOption = isRoleOption; + } + + public object ValueObject { get; set; } + public OptionBehaviour Setting { get; set; } + public CustomOptionType Type { get; set; } + public object DefaultValue { get; set; } + public static Func Seconds { get; } = value => $"{value:0.0#}s"; + public static Func Multiplier { get; } = value => $"{value:0.0#}x"; - public CustomOption(string title) + public string GetName() + { + return Name; + } + + public override string ToString() + { + return Format(ValueObject); + } + + public virtual void OptionCreated() + { + Setting.name = Setting.gameObject.name = GetName(); + } + + public void Set(object value, bool SendRpc = true, bool Notify = false) + { + //PeasAPI.Logger.LogInfo($"{Name} set to {value}"); + + ValueObject = value; + + if (Setting != null && AmongUsClient.Instance.AmHost && SendRpc) + Coroutines.Start(RpcUpdateSetting.SendRpc(this)); + + try + { + if (Setting is ToggleOption toggle) + { + var newValue = (bool)ValueObject; + toggle.oldValue = newValue; + if (toggle.CheckMark != null) toggle.CheckMark.enabled = newValue; + } + else if (Setting is NumberOption number) + { + var newValue = (float)ValueObject; + + number.Value = number.oldValue = newValue; + number.ValueText.text = ToString(); + } + else if (Setting is StringOption str) + { + var newValue = (int)ValueObject; + + str.Value = str.oldValue = newValue; + str.ValueText.text = ToString(); + } + } + catch { - Title = title; } + + if (HudManager.InstanceExists && Type != CustomOptionType.Header && Notify) + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, ToString(), + HudManager.Instance.Notifier.lastMessageKey != (int)StringName); } } \ No newline at end of file diff --git a/PeasAPI/Options/CustomOptionButton.cs b/PeasAPI/Options/CustomOptionButton.cs deleted file mode 100644 index 7dbec0a..0000000 --- a/PeasAPI/Options/CustomOptionButton.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using Reactor; -using Object = UnityEngine.Object; - -namespace PeasAPI.Options -{ - public class CustomOptionButton : CustomOption - { - public bool Value { get; private set; } - - public bool OldValue { get; private set; } - - public delegate void OnValueChangedHandler(CustomOptionButtonValueChangedArgs args); - - public event OnValueChangedHandler OnValueChanged; - - public class CustomOptionButtonValueChangedArgs - { - public CustomOptionButton Option; - - public bool OldValue; - - public bool NewValue; - - public CustomOptionButtonValueChangedArgs(CustomOptionButton option, bool oldValue, bool newValue) - { - Option = option; - OldValue = oldValue; - NewValue = newValue; - } - } - - public void SetValue(bool value) - { - var oldValue = !value; - - if (AmongUsClient.Instance.AmHost) - { - if (Option) - ((ToggleOption) Option).CheckMark.enabled = value; - - Value = value; - OldValue = oldValue; - - ValueChanged(value, oldValue); - } - else - { - if (Option) - ((StringOption) Option).Value = value ? 0 : 1; - - Value = value; - OldValue = oldValue; - - ValueChanged(value, oldValue); - } - } - - public void ValueChanged(bool newValue, bool oldValue) - { - var args = new CustomOptionButtonValueChangedArgs(this, oldValue, newValue); - OnValueChanged?.Invoke(args); - } - - internal OptionBehaviour CreateOption(ToggleOption toggleOptionPrefab, StringOption stringOptionPrefab) - { - if (AmongUsClient.Instance.AmHost) - { - ToggleOption toggleOption = - Object.Instantiate(toggleOptionPrefab, toggleOptionPrefab.transform.parent); - - Option = toggleOption; - - toggleOption.TitleText.text = Title; - toggleOption.Title = CustomStringName.Register(Title); - toggleOption.CheckMark.enabled = false; - toggleOption.transform.FindChild("CheckBox").gameObject.SetActive(false); - - toggleOption.OnValueChanged = new Action(behaviour => - { - SetValue(!toggleOption.oldValue); - }); - - return toggleOption; - } - else - { - StringOption toggleOption = - Object.Instantiate(stringOptionPrefab, stringOptionPrefab.transform.parent); - - Option = toggleOption; - - toggleOption.TitleText.text = Title; - toggleOption.Title = CustomStringName.Register(Title); - toggleOption.Value = 0; - toggleOption.transform.FindChild("Value_TMP").gameObject.SetActive(false); - - toggleOption.OnValueChanged = new Action(behaviour => - { - SetValue(Value); - }); - toggleOption.OnValueChanged.Invoke(toggleOption); - - return toggleOption; - } - } - - public CustomOptionButton(string id, string title, bool defaultValue) : base(title) - { - OptionManager.CustomOptions.Add(this); - } - } -} \ No newline at end of file diff --git a/PeasAPI/Options/CustomOptionHeader.cs b/PeasAPI/Options/CustomOptionHeader.cs deleted file mode 100644 index 4870aed..0000000 --- a/PeasAPI/Options/CustomOptionHeader.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Reactor; -using Reactor.Extensions; -using UnityEngine; - -namespace PeasAPI.Options -{ - public class CustomOptionHeader : CustomOption - { - public CustomOptionHeader(string title) : base(title) - { - OptionManager.CustomOptions.Add(this); - } - - internal OptionBehaviour CreateOption(ToggleOption toggleOptionPrefab) - { - ToggleOption header = - Object.Instantiate(toggleOptionPrefab, toggleOptionPrefab.transform.parent); - - header.TitleText.text = Title; - header.Title = CustomStringName.Register(Title); - - var checkBox = header.transform.FindChild("CheckBox")?.gameObject; - if (checkBox) checkBox.Destroy(); - - var background = header.transform.FindChild("Background")?.gameObject; - if (background) background.Destroy(); - - Option = header; - HudFormat = "{0}"; - - return header; - } - } -} \ No newline at end of file diff --git a/PeasAPI/Options/CustomOptionType.cs b/PeasAPI/Options/CustomOptionType.cs new file mode 100644 index 0000000..6a97d1d --- /dev/null +++ b/PeasAPI/Options/CustomOptionType.cs @@ -0,0 +1,26 @@ +namespace PeasAPI.Options; + +public enum CustomOptionType +{ + Header, + Toggle, + Number, + String, + Button +} + +public enum CustomRoleOptionType +{ + Chance, + Count, + None +} + +public enum MultiMenu +{ + Main, + Crewmate, + Neutral, + Impostor, + NULL +} \ No newline at end of file diff --git a/PeasAPI/Options/CustomRoleOption.cs b/PeasAPI/Options/CustomRoleOption.cs index 4d16721..b3c7a70 100644 --- a/PeasAPI/Options/CustomRoleOption.cs +++ b/PeasAPI/Options/CustomRoleOption.cs @@ -1,157 +1,81 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; -using HarmonyLib; using PeasAPI.Roles; -using Reactor; -using Reactor.Extensions; -using TMPro; -using UnityEngine; -using Object = UnityEngine.Object; -namespace PeasAPI.Options -{ - public class CustomRoleOption : CustomOption - { - public BaseRole Role; - - public int Count; - - public int Chance; - - public CustomOption[] AdvancedOptions; - - public string AdvancedOptionPrefix;public delegate void OnValueChangedHandler(CustomRoleOptionValueChangedArgs args); +namespace PeasAPI.Options; - public event OnValueChangedHandler OnValueChanged; +public class CustomRoleOption : CustomOption +{ + internal CustomNumberOption chanceOption; + internal CustomNumberOption countOption; + internal CustomOption[] AdvancedOptions; - public class CustomRoleOptionValueChangedArgs + public CustomRoleOption(BaseRole baseRole, string prefix, CustomOption[] advancedOptions, MultiMenu menu = MultiMenu.NULL) : base(num++, + menu == MultiMenu.NULL ? GetMultiMenu(baseRole) : menu, + Utility.ColorString(baseRole.Color, baseRole.Name), CustomOptionType.Header, 0, isRoleOption: true) + { + List removedOptions = new List(); + if (advancedOptions != null) { - public CustomRoleOption Option; - - public int Count; - - public int OldCount; - - public int Chance; - - public int OldChance; - - public CustomRoleOptionValueChangedArgs(CustomRoleOption option, int count, int oldCount, int chance, int oldChance) + foreach (var option in advancedOptions) { - Option = option; - Count = count; - OldCount = oldCount; - Chance = chance; - OldChance = oldChance; + if (option != null && CustomOption.AllOptions.Contains(option)) + { + removedOptions.Add(option); + CustomOption.AllOptions.Remove(option); + } } } - public void SetValue(int count, int chance) - { - var oldCount = Count; - var oldChance = Chance; + chanceOption = new CustomNumberOption(menu == MultiMenu.NULL ? GetMultiMenu(baseRole) : menu, + "Role Chance", 0, 10, 0, 100, PercentFormat, + CustomRoleOptionType.Chance, baseRole); + countOption = new CustomNumberOption(menu == MultiMenu.NULL ? GetMultiMenu(baseRole) : menu, + "Role Count", 1, 1, 1, 15, null, + CustomRoleOptionType.Count, baseRole); - Count = count; - Chance = chance; - - ValueChanged(count, oldCount, chance, oldChance); - } - - internal void ValueChanged(int count, int oldCount, int chance, int oldChance) + foreach (var option in removedOptions) { - var args = new CustomRoleOptionValueChangedArgs(this, count, oldCount, chance, oldChance); - OnValueChanged?.Invoke(args); + CustomOption.AllOptions.Add(option); } - internal OptionBehaviour CreateOption(RoleOptionSetting roleOptionPrefab) + AdvancedOptions = advancedOptions; + if (advancedOptions != null) { - if (Option != null) + foreach (var option in advancedOptions.Where(o => o != null)) { - return Option; + option.Name = $"{prefix}{option.Name}"; } - var newSetting = Object.Instantiate(roleOptionPrefab, roleOptionPrefab.transform.parent); - newSetting.name = Role.Name; - newSetting.Role = Role.RoleBehaviour; - newSetting.SetRole(PlayerControl.GameOptions.RoleOptions); - - if (!PlayerControl.GameOptions.RoleOptions.roleRates.ContainsKey(Role.RoleBehaviour.Role)) - PlayerControl.GameOptions.RoleOptions.roleRates[Role.RoleBehaviour.Role] = - new RoleOptionsData.RoleRate(); - var rates = PlayerControl.GameOptions.RoleOptions.roleRates[Role.RoleBehaviour.Role]; - Count = rates.MaxCount; - Chance = rates.Chance; - - Option = newSetting; - return newSetting; } - - internal AdvancedRoleSettingsButton CreateOptionObjects(GameObject roleTabTemplate) - { - if (OptionManager.NumberOptionPrefab == null || OptionManager.ToggleOptionPrefab == null || - OptionManager.StringOptionPrefab == null) - return null; - - var tab = Object.Instantiate(roleTabTemplate, roleTabTemplate.transform.parent); - tab.name = Role.Name + " Settings"; - - if (AdvancedOptions.Length == 0 && Option != null) - { - Option.transform.FindChild("More Options").gameObject.SetActive(false); - } - - foreach (var option in tab.GetComponentsInChildren()) - { - option.gameObject.DestroyImmediate(); - } - - foreach (var advancedOption in AdvancedOptions) - { - OptionBehaviour optionBehaviour = null; - switch (advancedOption) - { - case CustomNumberOption option: - optionBehaviour = option.CreateOption(OptionManager.NumberOptionPrefab); - break; - case CustomToggleOption option: - optionBehaviour = option.CreateOption(OptionManager.ToggleOptionPrefab, OptionManager.StringOptionPrefab); - break; - case CustomStringOption option: - optionBehaviour = option.CreateOption(OptionManager.StringOptionPrefab); - break; - } - - optionBehaviour.Title = CustomStringName.Register(advancedOption.Title); - optionBehaviour.name = advancedOption.Title; + } - var optionTransform = optionBehaviour.transform; - optionTransform.parent = tab.transform; - optionTransform.localScale = Vector3.one; - optionTransform.localPosition = - new Vector3(-1.25f, 0.06f - AdvancedOptions.ToList().IndexOf(advancedOption) * 0.56f, 0f); - } - - var roleName = tab.transform.FindChild("Role Name"); - roleName.GetComponent().Destroy(); - roleName.GetComponent().text = Role.Name; - - var advancedOptions = new AdvancedRoleSettingsButton - { - Tab = tab, - Type = Role.RoleBehaviour.Role - }; - - return advancedOptions; - } + public static Func PercentFormat { get; } = value => $"{value:0}%"; - public CustomRoleOption(BaseRole role, string advancedOptionPrefix, params CustomOption[] advancedOptions) : base(role.Name) + private static MultiMenu GetMultiMenu(BaseRole baseRole) + { + switch (baseRole.Team) { - Role = role; - AdvancedOptions = advancedOptions; - AdvancedOptions.Do(option => option.AdvancedRoleOption = true ); - AdvancedOptionPrefix = advancedOptionPrefix; - HudFormat = "{0}: {1} with {2}% Chance"; - - OptionManager.CustomRoleOptions.Add(this); + case Team.Role: + return MultiMenu.Neutral; + case Team.Alone: + return MultiMenu.Neutral; + case Team.Crewmate: + return MultiMenu.Crewmate; + case Team.Impostor: + return MultiMenu.Impostor; + default: + return MultiMenu.Main; } } + + public int GetChance() + { + return (int)chanceOption.Value; + } + + public int GetCount() + { + return (int)countOption.Value; + } } \ No newline at end of file diff --git a/PeasAPI/Options/CustomStringOption.cs b/PeasAPI/Options/CustomStringOption.cs index 286a0e4..c662d5f 100644 --- a/PeasAPI/Options/CustomStringOption.cs +++ b/PeasAPI/Options/CustomStringOption.cs @@ -1,118 +1,40 @@ -using System; -using BepInEx.Configuration; -using System.Collections.Generic; -using System.Reflection; -using BepInEx.IL2CPP; -using PeasAPI.CustomRpc; -using Reactor; -using Reactor.Networking; -using UnityEngine; -using Object = UnityEngine.Object; +namespace PeasAPI.Options; -namespace PeasAPI.Options +public class CustomStringOption : CustomOption { - public class CustomStringOption : CustomOption + public CustomStringOption(MultiMenu menu, string optionName, string[] values, + int startingId = 0) : + base(num++, menu, optionName, CustomOptionType.String, startingId) { - public int Value { get; private set; } - - public string StringValue - { - get - { - if (Values.Count >= Value + 1) - return Values[Value].GetTranslation(); - return "Error"; - } - } - - public int OldValue { get; private set; } - - public List Values { get; set; } - - public delegate void OnValueChangedHandler(CustomStringOptionValueChangedArgs args); - - public event OnValueChangedHandler OnValueChanged; - - private ConfigEntry _configEntry; - - public class CustomStringOptionValueChangedArgs - { - public CustomStringOption Option; - - public int OldValue; - - public int NewValue; - - public CustomStringOptionValueChangedArgs(CustomStringOption option, int oldValue, int newValue) - { - Option = option; - OldValue = oldValue; - NewValue = newValue; - } - } - - public void SetValue(int value) - { - var oldValue = Value; - - if (AmongUsClient.Instance.AmHost && _configEntry != null) - _configEntry.Value = value; - - Value = value; - if (Option != null) - ((StringOption) Option).Value = value; - OldValue = oldValue; - - ValueChanged(value, oldValue); + Values = values; + Format = value => Values[(int)value]; + } - if (AmongUsClient.Instance.AmHost) - Rpc.Instance.Send(new RpcUpdateSetting.Data(this, value)); - } - - public void ValueChanged(int newValue, int oldValue) - { - var args = new CustomStringOptionValueChangedArgs(this, oldValue, newValue); - OnValueChanged?.Invoke(args); - } + public string[] Values { get; set; } + public int Value => (int)ValueObject; + public string StringValue => Values[Value]; - internal OptionBehaviour CreateOption(StringOption stringOptionPrefab) - { - StringOption stringOption = - Object.Instantiate(stringOptionPrefab, stringOptionPrefab.transform.parent); - - stringOption.TitleText.text = Title; - stringOption.Title = CustomStringName.Register(Title); - stringOption.Value = Value; - stringOption.ValueText.text = StringValue; - stringOption.Values = Values.ToArray(); - - Option = stringOption; + public void Increase() + { + if (Value >= Values.Length - 1) + Set(0); + else + Set(Value + 1); + } - stringOption.OnValueChanged = new Action(behaviour => - { - SetValue(stringOption.Value); - }); - - return stringOption; - } - - public CustomStringOption(string id, string title, params string[] values) : base(title) - { - Id = $"{Assembly.GetCallingAssembly().GetName().Name}.StringOption.{id}"; - try - { - _configEntry = PeasAPI.ConfigFile.Bind("Options", Id, 0); - } catch (Exception e) { - PeasAPI.Logger.LogError($"Error while loading the option \"{title}\": {e.Source}"); - } - - Value = _configEntry?.Value ?? 0; - Values = new List(); - foreach (var value in values) - Values.Add((StringNames)CustomStringName.Register(value)); - HudFormat = "{0}: {1}"; + public void Decrease() + { + if (Value <= 0) + Set(Values.Length - 1); + else + Set(Value - 1); + } - OptionManager.CustomOptions.Add(this); - } + public override void OptionCreated() + { + base.OptionCreated(); + var str = Setting.Cast(); + str.Value = str.oldValue = Value; + str.ValueText.text = ToString(); } } \ No newline at end of file diff --git a/PeasAPI/Options/CustomToggleOption.cs b/PeasAPI/Options/CustomToggleOption.cs index 43f93e0..882400c 100644 --- a/PeasAPI/Options/CustomToggleOption.cs +++ b/PeasAPI/Options/CustomToggleOption.cs @@ -1,141 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using BepInEx.Configuration; -using PeasAPI.CustomRpc; -using Reactor; -using Reactor.Networking; -using UnityEngine; -using Object = System.Object; +namespace PeasAPI.Options; -namespace PeasAPI.Options +public class CustomToggleOption : CustomOption { - public class CustomToggleOption : CustomOption + public CustomToggleOption(MultiMenu menu, string optionName, bool value = true) : base( + num++, menu, optionName, CustomOptionType.Toggle, value) { - public bool Value { get; private set; } - - public bool OldValue { get; private set; } - - public delegate void OnValueChangedHandler(CustomToggleOptionValueChangedArgs args); - - public event OnValueChangedHandler OnValueChanged; - - private ConfigEntry _configEntry; - - public class CustomToggleOptionValueChangedArgs - { - public CustomToggleOption Option; - - public bool OldValue; - - public bool NewValue; - - public CustomToggleOptionValueChangedArgs(CustomToggleOption option, bool oldValue, bool newValue) - { - Option = option; - OldValue = oldValue; - NewValue = newValue; - } - } - - public void SetValue(bool value) - { - var oldValue = !value; - - if (AmongUsClient.Instance.AmHost) - { - if (_configEntry != null) - _configEntry.Value = value; - - if (Option) - ((ToggleOption) Option).CheckMark.enabled = value; - - Value = value; - OldValue = oldValue; - - ValueChanged(value, oldValue); - - Rpc.Instance.Send(new RpcUpdateSetting.Data(this, value)); - } - else - { - if (Option) - ((StringOption) Option).Value = value ? 0 : 1; - - Value = value; - OldValue = oldValue; - - ValueChanged(value, oldValue); - } - } - - internal void ValueChanged(bool newValue, bool oldValue) - { - var args = new CustomToggleOptionValueChangedArgs(this, oldValue, newValue); - OnValueChanged?.Invoke(args); - } - - internal OptionBehaviour CreateOption(ToggleOption toggleOptionPrefab, StringOption stringOptionPrefab) - { - if (AmongUsClient.Instance.AmHost) - { - ToggleOption toggleOption = - UnityEngine.Object.Instantiate(toggleOptionPrefab, toggleOptionPrefab.transform.parent); - - Option = toggleOption; - - toggleOption.TitleText.text = Title; - toggleOption.Title = CustomStringName.Register(Title); - toggleOption.CheckMark.enabled = Value; - - toggleOption.OnValueChanged = new Action(behaviour => - { - SetValue(!toggleOption.oldValue); - }); - - return toggleOption; - } - else - { - StringOption toggleOption = - UnityEngine.Object.Instantiate(stringOptionPrefab, stringOptionPrefab.transform.parent); - - Option = toggleOption; - - toggleOption.TitleText.text = Title; - toggleOption.Title = CustomStringName.Register(Title); - toggleOption.Value = Value ? 0 : 1; + Format = val => (bool)val ? "On" : "Off"; + } - var values = new List(); - values.Add(CustomStringName.Register("On")); - values.Add(CustomStringName.Register("Off")); - toggleOption.Values = values.ToArray(); + public bool Value => (bool)ValueObject; - toggleOption.OnValueChanged = new Action(behaviour => - { - SetValue(toggleOption.Value == 0); - }); + public void Toggle() + { + Set(!Value); + } - return toggleOption; - } - } - - public CustomToggleOption(string id, string title, bool defaultValue) : base(title) - { - Id = $"{Assembly.GetCallingAssembly().GetName().Name}.ToggleOption.{id}"; - try - { - _configEntry = PeasAPI.ConfigFile.Bind("Options", Id, defaultValue); - } - catch (Exception e) - { - PeasAPI.Logger.LogError($"Error while loading the option \"{title}\": {e.Source}"); - } - - Value = _configEntry?.Value ?? defaultValue; - HudFormat = "{0}: {1}"; - - OptionManager.CustomOptions.Add(this); - } + public override void OptionCreated() + { + base.OptionCreated(); + var tgl = Setting.Cast(); + tgl.CheckMark.enabled = Value; } } \ No newline at end of file diff --git a/PeasAPI/Options/OptionManager.cs b/PeasAPI/Options/OptionManager.cs deleted file mode 100644 index 92ac2bf..0000000 --- a/PeasAPI/Options/OptionManager.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; - -namespace PeasAPI.Options -{ - public class OptionManager - { - public static List CustomOptions = new List(); - - public static List CustomRoleOptions = new List(); - - public static List HudVisibleOptions => CustomOptions.FindAll(option => option.HudVisible); - - public static List MenuVisibleOptions => CustomOptions.FindAll(option => option.MenuVisible && !option.AdvancedRoleOption); - - public static ToggleOption ToggleOptionPrefab; - public static NumberOption NumberOptionPrefab; - public static StringOption StringOptionPrefab; - } -} \ No newline at end of file diff --git a/PeasAPI/Options/Patches.cs b/PeasAPI/Options/Patches.cs index deaa875..4335bb9 100644 --- a/PeasAPI/Options/Patches.cs +++ b/PeasAPI/Options/Patches.cs @@ -1,358 +1,680 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; using Il2CppSystem.Text; -using InnerNet; using PeasAPI.CustomRpc; -using Reactor.Extensions; -using Reactor.Networking; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; using UnityEngine; using Object = UnityEngine.Object; -namespace PeasAPI.Options +namespace PeasAPI.Options; + +public static class Patches { - [HarmonyPatch] - public static class Patches + [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.CreateSettings))] + private class MoreTasks { - private static float AllOptionSize = 6.73f; - private static float LowestOption = -7.85f; - private static float OptionSize = 0.5f; - private static float HudTextSize = 1.4f; - - private static Scroller OptionsScroller; - - [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Start))] - [HarmonyPostfix] - private static void GameOptionsMenuStartPatch(GameOptionsMenu __instance) + public static void Postfix(GameOptionsMenu __instance) { - var numberOptionPrefab = OptionManager.NumberOptionPrefab = Object.FindObjectsOfType().FirstOrDefault(); - - var toggleOptionPrefab = OptionManager.ToggleOptionPrefab = Object.FindObjectOfType(); - - var stringOptionPrefab = OptionManager.StringOptionPrefab = Object.FindObjectsOfType().FirstOrDefault(); - - LowestOption = 1.15f - __instance.Children.Length * 0.5f; + if (__instance.gameObject.name == "GAME SETTINGS TAB") + try + { + var commonTasks = __instance.Children.ToArray().FirstOrDefault(x => + x.TryCast()?.intOptionName == Int32OptionNames.NumCommonTasks) + .Cast(); + if (commonTasks != null) commonTasks.ValidRange = new FloatRange(0f, 4f); - foreach (var customOption in OptionManager.CustomOptions.Where(option => !option.AdvancedRoleOption)) - { - OptionBehaviour option = null; + var shortTasks = __instance.Children.ToArray() + .FirstOrDefault(x => x.TryCast()?.intOptionName == Int32OptionNames.NumShortTasks) + .Cast(); + if (shortTasks != null) shortTasks.ValidRange = new FloatRange(0f, 26f); - if (customOption.GetType() == typeof(CustomToggleOption)) - { - option = ((CustomToggleOption)customOption).CreateOption(toggleOptionPrefab, stringOptionPrefab); + var longTasks = __instance.Children.ToArray() + .FirstOrDefault(x => x.TryCast()?.intOptionName == Int32OptionNames.NumLongTasks) + .Cast(); + if (longTasks != null) longTasks.ValidRange = new FloatRange(0f, 15f); } - else if (customOption.GetType() == typeof(CustomNumberOption)) + catch { - option = ((CustomNumberOption) customOption).CreateOption(numberOptionPrefab); } - else if (customOption.GetType() == typeof(CustomStringOption)) - { - option = ((CustomStringOption) customOption).CreateOption(stringOptionPrefab); - } - else if (customOption.GetType() == typeof(CustomOptionHeader)) - { - option = ((CustomOptionHeader) customOption).CreateOption(toggleOptionPrefab); - } - else if (customOption.GetType() == typeof(CustomOptionButton)) + } + } + + [HarmonyPatch(typeof(GameSettingMenu), nameof(GameSettingMenu.ChangeTab))] + private class ChangeTab + { + public static void Postfix(GameSettingMenu __instance, int tabNum, bool previewOnly) + { + if (previewOnly) return; + foreach (var tab in SettingsUpdate.Tabs) + if (tab != null) + tab.SetActive(false); + foreach (var button in SettingsUpdate.Buttons) button.SelectButton(false); + if (tabNum > 2) + { + tabNum -= 3; + SettingsUpdate.Tabs[tabNum].SetActive(true); + + if (tabNum > 4) return; + SettingsUpdate.Buttons[tabNum].SelectButton(true); + + __instance.StartCoroutine(Effects.Lerp(1f, new Action(p => { - option = ((CustomOptionButton) customOption).CreateOption(toggleOptionPrefab, stringOptionPrefab); - } + foreach (var option in CustomOption.AllOptions) + if (option.Type == CustomOptionType.Number) + { + var number = option.Setting.Cast(); + number.TitleText.text = option.GetName(); + if (number.TitleText.text.StartsWith(" 20) + number.TitleText.fontSize = 2.25f; + else if (number.TitleText.text.Length > 40) + number.TitleText.fontSize = 2f; + else number.TitleText.fontSize = 2.75f; + } - option.transform.localPosition = new Vector3(option.transform.localPosition.x, - LowestOption + 2 * OptionSize - (OptionManager.CustomOptions.IndexOf(customOption) + 1) * OptionSize, -1); + else if (option.Type == CustomOptionType.Toggle) + { + var tgl = option.Setting.Cast(); + tgl.TitleText.text = option.GetName(); + if (tgl.TitleText.text.Length > 20) + tgl.TitleText.fontSize = 2.25f; + else if (tgl.TitleText.text.Length > 40) + tgl.TitleText.fontSize = 2f; + else tgl.TitleText.fontSize = 2.75f; + } - var options = __instance.Children.ToList(); - options.Add(option); - __instance.Children = options.ToArray(); + else if (option.Type == CustomOptionType.String) + { + var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; + var str = option.Setting.Cast(); + str.TitleText.text = option.GetName(); + if (str.TitleText.text.Length > 20) + str.TitleText.fontSize = 2.25f; + else if (str.TitleText.text.Length > 40) + str.TitleText.fontSize = 2f; + else str.TitleText.fontSize = 2.75f; + } + }))); } + } + } + + [HarmonyPatch(typeof(GameSettingMenu), nameof(GameSettingMenu.Close))] + private class CloseSettings + { + public static void Prefix(GameSettingMenu __instance) + { + LobbyInfoPane.Instance.EditButton.gameObject.SetActive(true); + } + } + + [HarmonyPatch(typeof(GameSettingMenu), nameof(GameSettingMenu.Start))] + internal class SettingsUpdate + { + public static List Buttons = new(); + public static List Tabs = new(); + + public static void Postfix(GameSettingMenu __instance) + { + LobbyInfoPane.Instance.EditButton.gameObject.SetActive(false); + Buttons.ForEach(x => x?.Destroy()); + Tabs.ForEach(x => x?.Destroy()); + Buttons = new List(); + Tabs = new List(); + + if (GameOptionsManager.Instance.currentGameOptions.GameMode == AmongUs.GameOptions.GameModes.HideNSeek) return; + + GameObject.Find("What Is This?")?.Destroy(); + GameObject.Find("RoleSettingsButton")?.Destroy(); + GameObject.Find("GamePresetButton")?.Destroy(); + __instance.ChangeTab(1, false); - __instance.GetComponentInParent().ContentYBounds.max = - AllOptionSize + OptionManager.MenuVisibleOptions.Count * 0.5f - 2 * OptionSize; + var settingsButton = GameObject.Find("GameSettingsButton"); + settingsButton.transform.localPosition += new Vector3(0f, 2f, 0f); + settingsButton.transform.localScale *= 0.9f; + + CreateSettings(__instance, 3, "ModSettings", "Mod Settings", settingsButton, MultiMenu.Main); + CreateSettings(__instance, 4, "CrewSettings", "Crewmate Settings", settingsButton, + MultiMenu.Crewmate); + CreateSettings(__instance, 5, "NeutralSettings", "Neutral Settings", settingsButton, + MultiMenu.Neutral); + CreateSettings(__instance, 6, "ImpSettings", "Impostor Settings", settingsButton, + MultiMenu.Impostor); } - [HarmonyPatch(typeof(GameOptionsMenu), nameof(GameOptionsMenu.Update))] - [HarmonyPostfix] - private static void GameOptionsMenuUpdatePatch(GameOptionsMenu __instance) + internal static TextMeshPro SpawnExternalButton(GameSettingMenu __instance, GameOptionsMenu tabOptions, + ref float num, string text, Action onClick) { - __instance.GetComponentInParent().ContentYBounds.max = - AllOptionSize + OptionManager.MenuVisibleOptions.Count * 0.5f - 2 * OptionSize; + const float scaleX = 7f; + var baseButton = __instance.GameSettingsTab.checkboxOrigin.transform.GetChild(1); + var baseText = __instance.GameSettingsTab.checkboxOrigin.transform.GetChild(0); + + var exportButtonGO = GameObject.Instantiate(baseButton, Vector3.zero, Quaternion.identity, + tabOptions.settingsContainer); + exportButtonGO.name = text; + exportButtonGO.transform.localPosition = new Vector3(1f, num, -2f); + exportButtonGO.GetComponent().offset = Vector2.zero; + exportButtonGO.name = text.Replace(" ", ""); + + var prevColliderSize = exportButtonGO.GetComponent().size; + prevColliderSize.x *= scaleX; + exportButtonGO.GetComponent().size = prevColliderSize; - foreach (var option in __instance.Children.ToList().FindAll(option => option.IsCustom())) + exportButtonGO.transform.GetChild(2).gameObject.DestroyImmediate(); + var exportButton = exportButtonGO.GetComponent(); + exportButton.ClickMask = tabOptions.ButtonClickMask; + exportButton.OnClick.RemoveAllListeners(); + exportButton.OnClick.AddListener(onClick); + + var exportButtonTextGO = GameObject.Instantiate(baseText, exportButtonGO); + exportButtonTextGO.transform.localPosition = new Vector3(0, 0, -3f); + exportButtonTextGO.GetComponent().SetSize(prevColliderSize.x, prevColliderSize.y); + var exportButtonText = exportButtonTextGO.GetComponent(); + exportButtonText.alignment = TextAlignmentOptions.Center; + exportButtonText.SetText(text); + + SpriteRenderer[] componentsInChildren = exportButtonGO.GetComponentsInChildren(true); + for (var i = 0; i < componentsInChildren.Length; i++) { - var customOption = option.GetCustom(); - if (customOption == null) - continue; - - option.gameObject.SetActive(customOption.MenuVisible); - - if (customOption.MenuVisible) - option.transform.localPosition = new Vector3(option.transform.localPosition.x, - LowestOption + 2 * OptionSize - (OptionManager.MenuVisibleOptions.IndexOf(customOption) + 1) * OptionSize, -1); - - if (option.gameObject.GetComponent() != null) - option.gameObject.GetComponent().TitleText.text = customOption.Title; - else if (option.gameObject.GetComponent() != null) - option.gameObject.GetComponent().TitleText.text = customOption.Title; - else if (option.gameObject.GetComponent() != null) - option.gameObject.GetComponent().TitleText.text = customOption.Title; + componentsInChildren[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + componentsInChildren[i].transform.localPosition = new Vector3(0, 0, -1); + var prevSpriteSize = componentsInChildren[i].size; + prevSpriteSize.x *= scaleX; + componentsInChildren[i].size = prevSpriteSize; } + + TextMeshPro[] componentsInChildren2 = exportButtonGO.GetComponentsInChildren(true); + foreach (var obj in componentsInChildren2) + { + obj.fontMaterial.SetFloat("_StencilComp", 3f); + obj.fontMaterial.SetFloat("_Stencil", 20); + } + + num -= 0.6f; + return exportButtonText; } - [HarmonyPatch(typeof(RolesSettingsMenu), nameof(RolesSettingsMenu.OnEnable))] - [HarmonyPostfix] - public static void RoleOptionCreatePatch(RolesSettingsMenu __instance) + public static void CreateSettings(GameSettingMenu __instance, int target, string name, string text, + GameObject settingsButton, MultiMenu menu) { - var roleSettingPrefab = __instance.AllRoleSettings.ToArray()[0]; - var roleTabPrefab = __instance.AllAdvancedSettingTabs.ToArray()[0].Tab; - foreach (var option in OptionManager.CustomRoleOptions) + var panel = GameObject.Find("LeftPanel"); + var button = GameObject.Find(name); + if (button == null) { - if (option.GetType() == typeof(CustomRoleOption)) + button = GameObject.Instantiate(settingsButton, panel.transform); + button.transform.localPosition += new Vector3(0f, -0.55f * target + 1.1f, 0f); + button.name = name; + __instance.StartCoroutine(Effects.Lerp(1f, + new Action(p => + { + button.transform.FindChild("FontPlacer").GetComponentInChildren().text = + text; + }))); + var passiveButton = button.GetComponent(); + passiveButton.OnClick.RemoveAllListeners(); + passiveButton.OnClick.AddListener((Action)(() => { __instance.ChangeTab(target, false); })); + passiveButton.SelectButton(false); + Buttons.Add(passiveButton); + } + + var settingsTab = GameObject.Find("GAME SETTINGS TAB"); + Tabs.RemoveAll(x => x == null); + var tab = GameObject.Instantiate(settingsTab, settingsTab.transform.parent); + tab.name = name; + var tabOptions = tab.GetComponent(); + foreach (var child in tabOptions.Children) child.Destroy(); + tabOptions.scrollBar.transform.FindChild("SliderInner").DestroyChildren(); + tabOptions.Children.Clear(); + var options = CustomOption.AllOptions.Where(x => x.Menu == menu).ToList(); + + if (target < 8) + { + var num = 1.5f; + + foreach (var option in options) { - var newSetting = option.CreateOption(roleSettingPrefab); - newSetting.transform.localPosition = roleSettingPrefab.transform.localPosition - new Vector3(0f , (__instance.AllRoleSettings.ToArray().Count + OptionManager.CustomRoleOptions.IndexOf(option) + 1) * 0.5f); - - var tab = option.CreateOptionObjects(roleTabPrefab); - if (tab != null) - __instance.AllAdvancedSettingTabs.Add(tab); + if (option.Type == CustomOptionType.Header) + { + var header = Object.Instantiate(tabOptions.categoryHeaderOrigin, Vector3.zero, + Quaternion.identity, tabOptions.settingsContainer); + header.SetHeader(StringNames.ImpostorsCategory, 20); + header.Title.text = option.GetName(); + header.transform.localScale = Vector3.one * 0.65f; + header.transform.localPosition = new Vector3(-0.9f, num, -2f); + num -= 0.625f; + continue; + } + + if (option.Type == CustomOptionType.Number) + { + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.numberOptionOrigin, + Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); + optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + + var numberOption = optionBehaviour as NumberOption; + option.Setting = numberOption; + + tabOptions.Children.Add(optionBehaviour); + } + + else if (option.Type == CustomOptionType.Toggle) + { + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.checkboxOrigin, Vector3.zero, + Quaternion.identity, tabOptions.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); + optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + + var toggleOption = optionBehaviour as ToggleOption; + option.Setting = toggleOption; + + tabOptions.Children.Add(optionBehaviour); + } + + else if (option.Type == CustomOptionType.String) + { + var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; + + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.stringOptionOrigin, + Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); + optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + + var stringOption = optionBehaviour as StringOption; + option.Setting = stringOption; + + tabOptions.Children.Add(optionBehaviour); + } + + num -= 0.45f; + tabOptions.scrollBar.SetYBoundsMax(-num - 1.65f); + option.OptionCreated(); } } - var scroller = roleSettingPrefab.gameObject.transform.parent.parent.GetComponent(); - scroller.ContentYBounds.max = (OptionManager.CustomRoleOptions.Count - 3) * 0.5f; - scroller.transform.FindChild("UI_Scrollbar").gameObject.SetActive(true); + for (var i = 0; i < tabOptions.Children.Count; i++) + { + var optionBehaviour = tabOptions.Children[i]; + if (AmongUsClient.Instance && !AmongUsClient.Instance.AmHost) optionBehaviour.SetAsPlayer(); + } + + Tabs.Add(tab); + tab.SetActive(false); } - [HarmonyPatch(typeof(RolesSettingsMenu), nameof(RolesSettingsMenu.ValueChanged))] - [HarmonyPostfix] - public static void RoleOptionValueChangedPatch(RolesSettingsMenu __instance, [HarmonyArgument(0)] OptionBehaviour obj) + public static void ImportSlot(string preset) { - var custom = obj.GetCustom(); - if (custom != null) + System.Console.WriteLine(preset); + + string text; + + try + { + var path = Path.Combine(Application.persistentDataPath, $"{preset}.txt"); + text = File.ReadAllText(path); + } + catch { - switch (custom) + return; + } + + var splitText = text.Split("\n").ToList(); + + while (splitText.Count > 0) + { + var name = splitText[0].Trim(); + splitText.RemoveAt(0); + var option = + CustomOption.AllOptions.FirstOrDefault(o => o.GetName().Equals(name, StringComparison.Ordinal)); + if (option == null) { - case CustomRoleOption option: - var rates = PlayerControl.GameOptions.RoleOptions.roleRates[option.Role.RoleBehaviour.Role]; - option.SetValue(rates.MaxCount, rates.Chance); - break; - case CustomNumberOption option: - option.SetValue(obj.GetFloat()); + try + { + splitText.RemoveAt(0); + } + catch + { + } + + continue; + } + + var value = splitText[0]; + splitText.RemoveAt(0); + switch (option.Type) + { + case CustomOptionType.Number: + option.Set(float.Parse(value), false); break; - case CustomToggleOption option: - option.SetValue(obj.GetBool()); + case CustomOptionType.Toggle: + option.Set(bool.Parse(value), false); break; - case CustomStringOption option: - option.SetValue(obj.GetInt()); + case CustomOptionType.String: + option.Set(int.Parse(value), false); break; } } + + Coroutines.Start(RpcUpdateSetting.SendRpc()); } - - [HarmonyPatch(typeof(OptionBehaviour), nameof(OptionBehaviour.SetAsPlayer))] - public static class OptionBehaviourSetAsPlayerPatch + + public static void ExportSlot(string preset) { - public static bool Prefix(OptionBehaviour __instance) + System.Console.WriteLine($"Exporting settings to {preset}"); + + var builder = new StringBuilder(); + foreach (var option in CustomOption.AllOptions) { - foreach (var button in __instance.GetComponentsInChildren()) - { - button.Destroy(); - button.gameObject.SetActive(button.gameObject.name == __instance.gameObject.name && - __instance.Title != StringNames.GameRecommendedSettings); - } - + if (option.Type is CustomOptionType.Button or CustomOptionType.Header) continue; + builder.AppendLine(option.GetName()); + builder.AppendLine($"{option.ValueObject}"); + } + + try + { + var path = Path.Combine(Application.persistentDataPath, $"{preset}.txt"); + File.WriteAllText(path, builder.ToString()); + } + catch + { + } + } + } + + [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.SetTab))] + private class SetTabPane + { + public static bool Prefix(LobbyViewSettingsPane __instance) + { + if ((int)__instance.currentTab < 6) + { + ChangeTabPane.Postfix(__instance, __instance.currentTab); return false; } + + return true; } - - [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.FixedUpdate))] - [HarmonyPostfix] - private static void NumberOptionFixedUpdatePatch(NumberOption __instance) + } + + [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.ChangeTab))] + private class ChangeTabPane + { + public static void Postfix(LobbyViewSettingsPane __instance, StringNames category) { - var customOption = (CustomNumberOption) __instance.GetCustom(); - if (customOption != null) + var tab = (int)category; + + foreach (var button in SettingsAwake.Buttons) button.SelectButton(false); + if (tab > 5) return; + __instance.taskTabButton.SelectButton(false); + + if (tab > 0) { - if (__instance.SuffixType == NumberSuffixes.None) - { - __instance.ValueText.text = customOption.Value.ToString(); - return; - } + tab -= 1; + SettingsAwake.Buttons[tab].SelectButton(true); + SettingsAwake.AddSettings(__instance, SettingsAwake.ButtonTypes[tab]); + } + } + } + + [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.Update))] + private class UpdatePane + { + public static void Postfix(LobbyViewSettingsPane __instance) + { + if (SettingsAwake.Buttons.Count == 0) SettingsAwake.Postfix(__instance); + } + } + + [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.Awake))] + private class SettingsAwake + { + public static readonly List Buttons = new(); + public static readonly List ButtonTypes = new(); + + public static void Postfix(LobbyViewSettingsPane __instance) + { + Buttons.ForEach(x => x?.Destroy()); + Buttons.Clear(); + ButtonTypes.Clear(); + + if (GameOptionsManager.Instance.currentGameOptions.GameMode == AmongUs.GameOptions.GameModes.HideNSeek) return; - if (__instance.SuffixType == NumberSuffixes.Multiplier) + GameObject.Find("RolesTabs")?.Destroy(); + var overview = GameObject.Find("OverviewTab"); + overview.transform.localScale += new Vector3(-0.35f, 0f, 0f); + overview.transform.localPosition += new Vector3(-1f, 0f, 0f); + overview.transform.GetChild(0).GetChild(0).transform.localScale += new Vector3(0.35f, 0f, 0f); + overview.transform.GetChild(0).GetChild(0).transform.localPosition += new Vector3(-1f, 0f, 0f); + + CreateButton(__instance, 1, "ModTab", "Mod Settings", MultiMenu.Main, overview); + CreateButton(__instance, 2, "CrewmateTab", "Crewmate Settings", MultiMenu.Crewmate, overview); + CreateButton(__instance, 3, "NeutralTab", "Neutral Settings", MultiMenu.Neutral, overview); + CreateButton(__instance, 4, "ImpostorTab", "Impostor Settings", MultiMenu.Impostor, overview); + } + + public static void CreateButton(LobbyViewSettingsPane __instance, int target, string name, string text, + MultiMenu menu, GameObject overview) + { + var tab = GameObject.Find(name); + if (tab == null) + { + tab = GameObject.Instantiate(overview, overview.transform.parent); + tab.transform.localPosition += new Vector3(2.5f, 0f, 0f) * target; + tab.transform.GetChild(0).GetChild(0).transform.localPosition += new Vector3(-0.5f, 0f, 0f); + tab.name = name; + __instance.StartCoroutine(Effects.Lerp(1f, + new Action(p => + { + tab.transform.FindChild("FontPlacer").GetComponentInChildren().text = + text; + }))); + var pTab = tab.GetComponent(); + pTab.OnClick.RemoveAllListeners(); + pTab.OnClick.AddListener((Action)(() => { __instance.ChangeTab((StringNames)target); })); + pTab.SelectButton(false); + Buttons.Add(pTab); + ButtonTypes.Add(menu); + } + } + + public static void AddSettings(LobbyViewSettingsPane __instance, MultiMenu menu) + { + var options = CustomOption.AllOptions.Where(x => x.Menu == menu).ToList(); + + var num = 1.5f; + var headingCount = 0; + var settingsThisHeader = 0; + var settingRowCount = 0; + + for (int j = 0; j < __instance.settingsInfo.Count; j++) + { + __instance.settingsInfo[j].gameObject.Destroy(); + } + + __instance.settingsInfo.Clear(); + + foreach (var option in options) + if (option.Type == CustomOptionType.Header) { - __instance.ValueText.text = customOption.Value + "x"; - return; + if (settingsThisHeader % 2 != 0) num -= 0.85f; + var header = Object.Instantiate(__instance.categoryHeaderOrigin); + header.SetHeader(StringNames.ImpostorsCategory, 61); + header.Title.text = option.GetName(); + header.transform.SetParent(__instance.settingsContainer); + header.transform.localScale = Vector3.one; + header.transform.localPosition = new Vector3(-9.8f, num, -2f); + __instance.settingsInfo.Add(header.gameObject); + num -= 1f; + headingCount += 1; + settingsThisHeader = 0; } - - if (__instance.SuffixType == NumberSuffixes.Seconds) + else { - __instance.ValueText.text = customOption.Value + "s"; - return; + var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; + if (option.Name.StartsWith("Slot ")) continue; + + var panel = Object.Instantiate(__instance.infoPanelOrigin); + panel.transform.SetParent(__instance.settingsContainer); + panel.transform.localScale = Vector3.one; + if (settingsThisHeader % 2 != 0) + { + panel.transform.localPosition = new Vector3(-3f, num, -2f); + num -= 0.85f; + } + else + { + settingRowCount += 1; + panel.transform.localPosition = new Vector3(-9f, num, -2f); + } + + settingsThisHeader += 1; + panel.SetInfo(StringNames.ImpostorsCategory, option.ToString(), 61); + panel.titleText.text = option.GetName(); + __instance.settingsInfo.Add(panel.gameObject); } - __instance.ValueText.text = customOption.Value.ToString(); - - } + float actual_spacing = (headingCount * 1.05f + settingRowCount * 0.85f) / (headingCount + settingRowCount) * 1.01f; + __instance.scrollBar.CalculateAndSetYBounds(__instance.settingsInfo.Count + (headingCount + settingRowCount) * 2 + headingCount, 2f, 6f, actual_spacing); } + } - private static bool OnModdedPage; - - [HarmonyPatch(typeof(KeyboardJoystick), nameof(KeyboardJoystick.Update))] - [HarmonyPostfix] - private static void SwitchSettingsPagesPatch(KeyboardJoystick __instance) + [HarmonyPatch(typeof(PlayerPhysics), nameof(PlayerPhysics.CoSpawnPlayer))] + private class PlayerJoinPatch + { + public static void Postfix(PlayerPhysics __instance) { - if (Input.GetKeyDown(KeyCode.RightShift)) - OnModdedPage = !OnModdedPage; + if (PlayerControl.AllPlayerControls.Count < 2 || !AmongUsClient.Instance || + !PlayerControl.LocalPlayer || !AmongUsClient.Instance.AmHost) return; + + Coroutines.Start(RpcUpdateSetting.SendRpc(RecipientId: __instance.myPlayer.OwnerId)); } - - [HarmonyPatch(typeof(GameOptionsData), nameof(GameOptionsData.ToHudString))] - [HarmonyPrefix] - private static bool AddInformationPatch(GameOptionsData __instance) + } + + + [HarmonyPatch(typeof(ToggleOption), nameof(ToggleOption.Toggle))] + private class ToggleButtonPatch + { + public static bool Prefix(ToggleOption __instance) { - if (OnModdedPage) + var option = + CustomOption.AllOptions.FirstOrDefault(option => + option.Setting == __instance); // Works but may need to change to gameObject.name check + if (option is CustomToggleOption toggle) { - __instance.settings.Length = 0; - __instance.settings.AppendLine("Press RightShift to switch to the vanilla settings"); - __instance.settings.AppendLine(); - - __instance.settings.AppendLine("Roles:"); - foreach (var option in OptionManager.CustomRoleOptions) - { - __instance.settings.AppendLine(String.Format(option.HudFormat, $"{option.Role.Color.ToTextColor()}{option.Role.Name}{Utility.StringColor.Reset}", - __instance.RoleOptions.GetNumPerGame(option.Role.RoleBehaviour.Role), - __instance.RoleOptions.GetChancePerGame(option.Role.RoleBehaviour.Role))); - option.AdvancedOptions.Where(_option => _option.HudVisible).Do(_option => RenderOption(_option, __instance.settings, option.AdvancedOptionPrefix) ); - } - - OptionManager.HudVisibleOptions.Where(option => !option.IsFromPeasAPI && !option.AdvancedRoleOption).Do(option => RenderOption(option, __instance.settings) ); - + toggle.Toggle(); return false; } - return true; + + if (GameOptionsManager.Instance.currentGameOptions.GameMode == AmongUs.GameOptions.GameModes.HideNSeek || + __instance.boolOptionName == BoolOptionNames.VisualTasks || + __instance.boolOptionName == BoolOptionNames.AnonymousVotes || + __instance.boolOptionName == BoolOptionNames.ConfirmImpostor) return true; + return false; } - - [HarmonyPatch(typeof(GameOptionsData), nameof(GameOptionsData.ToHudString))] - [HarmonyPostfix] - private static void GameOptionsDataToHudStringPatch(GameOptionsData __instance, ref string __result) + } + + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Initialize))] + private class NumberOptionInitialise + { + public static bool Prefix(NumberOption __instance) { - if (!OnModdedPage) + var option = + CustomOption.AllOptions.FirstOrDefault(option => + option.Setting == __instance); + if (option is CustomNumberOption number) { - var text = __instance.settings.ToString(); - __instance.settings.Clear(); - __instance.settings.AppendLine("Press RightShift to switch to the modded settings"); - __instance.settings.AppendLine(); - __instance.settings.AppendLine(text); - - OptionManager.HudVisibleOptions.Where(option => option.IsFromPeasAPI).Do(option => RenderOption(option, __instance.settings) ); + __instance.MinusBtn.isInteractable = true; + __instance.PlusBtn.isInteractable = true; + return false; } - __result = __instance.settings.ToString(); + return true; } + } - internal static void RenderOption(CustomOption option, StringBuilder builder, string prefix = "") + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Increase))] + private class NumberOptionPatchIncrease + { + public static bool Prefix(NumberOption __instance) { - switch (option) + var option = + CustomOption.AllOptions.FirstOrDefault(option => + option.Setting == __instance); // Works but may need to change to gameObject.name check + if (option is CustomNumberOption number) { - case CustomToggleOption _option: - builder.AppendLine(prefix + String.Format(_option.HudFormat, _option.Title, _option.Value ? "On" : "Off") + Utility.StringColor.Reset); - break; - case CustomNumberOption _option: - builder.AppendLine(prefix + String.Format(_option.HudFormat, _option.Title, _option.Value, _option.SuffixType switch - { - NumberSuffixes.None => "", - NumberSuffixes.Multiplier => "x", - NumberSuffixes.Seconds => "s", - _ => "" - }) + Utility.StringColor.Reset); - break; - case CustomStringOption _option: - builder.AppendLine(prefix + String.Format(_option.HudFormat, _option.Title, _option.StringValue) + Utility.StringColor.Reset); - break; - case CustomOptionHeader _option: - builder.AppendLine(prefix + String.Format(_option.HudFormat, _option.Title) + Utility.StringColor.Reset); - break; + number.Increase(); + return false; } - } - [HarmonyPatch(typeof(HudManager), nameof(HudManager.Update))] - [HarmonyPostfix] - private static void HudManagerUpdatePatch(HudManager __instance) - { - if (__instance.GameSettings == null) - return; - - __instance.GameSettings.fontSizeMin = - __instance.GameSettings.fontSizeMax = - __instance.GameSettings.fontSize = HudTextSize; - - CreateScroller(__instance); - - var bottomLeft = Camera.main.ScreenToWorldPoint(new Vector3(0, 0, 0)) - Camera.main.transform.localPosition; - - OptionsScroller.ContentYBounds = new FloatRange(-bottomLeft.y, Mathf.Max(-bottomLeft.y, __instance.GameSettings.renderedHeight - -bottomLeft.y + 0.02F)); + return true; } + } - //THIS BIT IS SKIDDED FROM ESSENTIALS: https://github.com/DorCoMaNdO/Reactor-Essentials - private static void CreateScroller(HudManager hudManager) + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Decrease))] + private class NumberOptionPatchDecrease + { + public static bool Prefix(NumberOption __instance) { - if (OptionsScroller != null) return; - - OptionsScroller = new GameObject("OptionsScroller").AddComponent(); - OptionsScroller.transform.SetParent(hudManager.GameSettings.transform.parent); - OptionsScroller.gameObject.layer = 5; - - OptionsScroller.transform.localScale = Vector3.one; - OptionsScroller.allowX = false; - OptionsScroller.allowY = true; - OptionsScroller.active = true; - OptionsScroller.velocity = new Vector2(0, 0); - OptionsScroller.ContentYBounds = new FloatRange(0, 0); - OptionsScroller.enabled = true; - - OptionsScroller.Inner = hudManager.GameSettings.transform; - hudManager.GameSettings.transform.SetParent(OptionsScroller.transform); + var option = + CustomOption.AllOptions.FirstOrDefault(option => + option.Setting == __instance); // Works but may need to change to gameObject.name check + if (option is CustomNumberOption number) + { + number.Decrease(); + return false; + } + + return true; } + } - [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnGameJoined))] - [HarmonyPostfix] - private static void RoleOptionInitialisePatch(AmongUsClient __instance) + [HarmonyPatch(typeof(StringOption), nameof(StringOption.Increase))] + private class StringOptionPatchIncrease + { + public static bool Prefix(StringOption __instance) { - if (!__instance.AmHost) - return; - - foreach (var option in OptionManager.CustomRoleOptions) + var option = CustomOption.AllOptions.FirstOrDefault(option => option.Setting == __instance); + if (option is CustomStringOption str) { - if (!PlayerControl.GameOptions.RoleOptions.roleRates.ContainsKey(option.Role.RoleBehaviour.Role)) - PlayerControl.GameOptions.RoleOptions.roleRates[option.Role.RoleBehaviour.Role] = - new RoleOptionsData.RoleRate(); - var rates = PlayerControl.GameOptions.RoleOptions.roleRates[option.Role.RoleBehaviour.Role]; - option.Count = rates.MaxCount; - option.Chance = rates.Chance; + str.Increase(); + + return false; } + + return true; } + } - [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.OnPlayerJoined))] - [HarmonyPostfix] - private static void AmongUsClientOnPlayerJoinedPatch(AmongUsClient __instance, - [HarmonyArgument(0)] ClientData client) + [HarmonyPatch(typeof(StringOption), nameof(StringOption.Decrease))] + private class StringOptionPatchDecrease + { + public static bool Prefix(StringOption __instance) { - if (__instance.AmHost) + var option = CustomOption.AllOptions.FirstOrDefault(option => option.Setting == __instance); + if (option is CustomStringOption str) { - foreach (var option in OptionManager.CustomOptions) - { - if (option.GetType() == typeof(CustomToggleOption)) - { - Rpc.Instance.SendTo(client.Id, new RpcUpdateSetting.Data(option, ((CustomToggleOption) option).Value)); - } - else if (option.GetType() == typeof(CustomNumberOption)) - { - Rpc.Instance.SendTo(client.Id, new RpcUpdateSetting.Data(option, ((CustomNumberOption) option).Value)); - } - else if (option.GetType() == typeof(CustomStringOption)) - { - Rpc.Instance.SendTo(client.Id, new RpcUpdateSetting.Data(option, ((CustomStringOption) option).Value)); - } - } + str.Decrease(); + + return false; } + + return true; } } } \ No newline at end of file diff --git a/PeasAPI/Patches.cs b/PeasAPI/Patches.cs index 30042d3..f46b3e3 100644 --- a/PeasAPI/Patches.cs +++ b/PeasAPI/Patches.cs @@ -1,19 +1,18 @@ using HarmonyLib; -using UnityEngine; namespace PeasAPI { [HarmonyPatch] public static class Patches { - [HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] + /*[HarmonyPatch(typeof(VersionShower), nameof(VersionShower.Start))] [HarmonyPostfix] public static void ChangeZOfAccountTabPatch() { var tab = AccountManager.Instance.accountTab; tab.transform.SetZ(1f); tab.GetComponent().computedClosedPosition = tab.GetComponent().computedClosedPosition.SetZ(1); - } + }*/ [HarmonyPatch(typeof(AccountManager), nameof(AccountManager.RandomizeName))] [HarmonyPrefix] diff --git a/PeasAPI/PeasAPI.cs b/PeasAPI/PeasAPI.cs index f1882e1..40e734b 100644 --- a/PeasAPI/PeasAPI.cs +++ b/PeasAPI/PeasAPI.cs @@ -1,7 +1,7 @@ using BepInEx; using BepInEx.Configuration; -using BepInEx.IL2CPP; using BepInEx.Logging; +using BepInEx.Unity.IL2CPP; using HarmonyLib; using InnerNet; using PeasAPI.Components; @@ -10,6 +10,7 @@ using PeasAPI.Options; using Reactor; using UnityEngine; +using static PeasAPI.Managers.WatermarkManager; using Random = System.Random; namespace PeasAPI @@ -21,7 +22,7 @@ namespace PeasAPI public class PeasAPI : BasePlugin { public const string Id = "tk.peasplayer.amongus.api"; - public const string Version = "1.8.3"; + public const string Version = "1.9.0"; public Harmony Harmony { get; } = new Harmony(Id); @@ -47,7 +48,7 @@ public static bool GameStarted { return GameData.Instance && ShipStatus.Instance && AmongUsClient.Instance && (AmongUsClient.Instance.GameState == InnerNetClient.GameStates.Started || - AmongUsClient.Instance.GameMode == global::GameModes.FreePlay); + AmongUsClient.Instance.NetworkMode == global::NetworkModes.FreePlay); } } @@ -75,10 +76,11 @@ public override void Load() RegisterCustomRoleAttribute.Load(); RegisterCustomGameModeAttribute.Load(); - + + new CustomHeaderOption(MultiMenu.Main, "General Settings"); ShowRolesOfDead = - new CustomToggleOption("ShowRolesOfDead", "Show the roles of dead player", false) {IsFromPeasAPI = true}; - GameModeManager.GameModeOption = new CustomStringOption("gamemode", "GameMode", "None") {IsFromPeasAPI = true}; + new CustomToggleOption(MultiMenu.Main, "Show the roles of dead player", false); + GameModeManager.GameModeOption = new CustomStringOption(MultiMenu.Main, "GameMode", new string[] { "None" }); Harmony.PatchAll(); } diff --git a/PeasAPI/PeasAPI.csproj b/PeasAPI/PeasAPI.csproj index 92acbe8..eef86cc 100644 --- a/PeasAPI/PeasAPI.csproj +++ b/PeasAPI/PeasAPI.csproj @@ -1,19 +1,16 @@  - netstandard2.1 - 1.8.3 + net6.0 + 1.9.0 release API for making Among Us mods Peasplayer latest - Steam - 2022.3.29 - 2022.3.29 - + PeasAPI-R PeasAPI-Icon.png git - https://github.com/Peasplayer/PeasAPI + https://github.com/fangkuaiclub/PeasAPI-R AGPL-3.0-only true true @@ -22,10 +19,11 @@ - - - - + + + + + diff --git a/PeasAPI/Roles/BaseRole.cs b/PeasAPI/Roles/BaseRole.cs index 3e9212e..c49bbe3 100644 --- a/PeasAPI/Roles/BaseRole.cs +++ b/PeasAPI/Roles/BaseRole.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using BepInEx.IL2CPP; +using BepInEx.Unity.IL2CPP; using PeasAPI.Managers; using PeasAPI.Options; using UnityEngine; @@ -60,12 +60,11 @@ public abstract class BaseRole /// /// How many player should get the Role /// + public virtual int Chance { get; set; } = 0; + public virtual int Count { get; set; } = 0; - public virtual int MaxCount { get; set; } = 15; - public virtual int Chance { get; set; } = 100; - public virtual bool CreateRoleOption { get; set; } = true; public CustomRoleOption Option; @@ -76,7 +75,7 @@ public abstract class BaseRole public virtual Type[] GameModeWhitelist { get; } = Array.Empty(); - public virtual float KillDistance { get; set; } = GameOptionsData.KillDistances[Mathf.Clamp(PlayerControl.GameOptions.KillDistance, 0, 2)]; + public virtual float KillDistance { get; set; } = Mathf.Clamp(GameManager.Instance?.LogicOptions?.GetKillDistance() ?? 1.8f, 0, 2); /// /// If a member of the role should be able to kill that player / in general @@ -118,7 +117,7 @@ public virtual bool _IsRoleVisible(PlayerControl playerWithRole, PlayerControl p switch (this.Visibility) { - case Visibility.Role: return perspective.IsRole(this); + case Visibility.Role: return perspective.IsCustomRole(this); case Visibility.Impostor: return perspective.Data.Role.IsImpostor; case Visibility.Crewmate: return true; case Visibility.NoOne: return false; @@ -127,38 +126,6 @@ public virtual bool _IsRoleVisible(PlayerControl playerWithRole, PlayerControl p } } - /// - /// This method calculates the nearest player to kill for a member of this role - /// - public virtual PlayerControl FindClosestTarget(PlayerControl from, bool protecting) - { - PlayerControl result = null; - float num = KillDistance; - if (!ShipStatus.Instance) - { - return null; - } - Vector2 truePosition = from.GetTruePosition(); - foreach (var playerInfo in GameData.Instance.AllPlayers) - { - if (!playerInfo.Disconnected && playerInfo.PlayerId != from.PlayerId && !playerInfo.IsDead && (from.GetRole().CanKill(playerInfo.Object) || protecting) && !playerInfo.Object.inVent) - { - PlayerControl @object = playerInfo.Object; - if (@object && @object.Collider.enabled) - { - Vector2 vector = @object.GetTruePosition() - truePosition; - float magnitude = vector.magnitude; - if (magnitude <= num && !PhysicsHelpers.AnyNonTriggersBetween(truePosition, vector.normalized, magnitude, Constants.ShipAndObjectsMask)) - { - result = @object; - num = magnitude; - } - } - } - } - return result; - } - public virtual bool ShouldGameEnd(GameOverReason reason) => true; /// @@ -184,10 +151,10 @@ internal void _OnUpdate() continue; if (PlayerControl.LocalPlayer == null) continue; - if (playerControl.IsRole(this) && _IsRoleVisible(playerControl, PlayerControl.LocalPlayer)) + if (playerControl.IsCustomRole(this) && _IsRoleVisible(playerControl, PlayerControl.LocalPlayer)) { - playerControl.nameText.color = this.Color; - playerControl.nameText.text = $"{player.GetPlayer().name}\n{Name}"; + playerControl.cosmetics.nameText.color = this.Color; + playerControl.cosmetics.nameText.text = $"{player.GetPlayer().name}\n{Name}"; } } @@ -213,10 +180,10 @@ internal void _OnMeetingUpdate(MeetingHud __instance) continue; if (PlayerControl.LocalPlayer == null) continue; - if (playerControl.IsRole(this) && _IsRoleVisible(playerControl, PlayerControl.LocalPlayer)) + if (playerControl.IsCustomRole(this) && _IsRoleVisible(playerControl, PlayerControl.LocalPlayer)) { - playerControl.nameText.color = this.Color; - playerControl.nameText.text = $"{player.GetPlayer().name}\n{Name}"; + playerControl.cosmetics.nameText.color = this.Color; + playerControl.cosmetics.nameText.text = $"{player.GetPlayer().name}\n{Name}"; } } @@ -227,7 +194,7 @@ internal void _OnMeetingUpdate(MeetingHud __instance) continue; if (PlayerControl.LocalPlayer == null) continue; - if (player.IsRole(this) && _IsRoleVisible(player, PlayerControl.LocalPlayer)) + if (player.IsCustomRole(this) && _IsRoleVisible(player, PlayerControl.LocalPlayer)) { pstate.NameText.color = Color; pstate.NameText.text = $"{player.name}\n{Name}"; @@ -273,16 +240,6 @@ public virtual void OnRevive(PlayerControl player) public virtual void OnTaskComplete(PlayerControl player, PlayerTask task) { } - - public int GetCount() - { - return Option?.Count ?? Count; - } - - public int GetChance() - { - return Option?.Chance ?? Chance; - } public BaseRole(BasePlugin plugin) { diff --git a/PeasAPI/Roles/ModRole.cs b/PeasAPI/Roles/ModRole.cs new file mode 100644 index 0000000..7765b4d --- /dev/null +++ b/PeasAPI/Roles/ModRole.cs @@ -0,0 +1,86 @@ +using Il2CppSystem.Text; +using PeasAPI; +using Reactor.Utilities.Attributes; + +namespace PeasAPI.Roles; + +[RegisterInIl2Cpp] +public class ModRole : RoleBehaviour +{ + public override bool IsDead => false; + + public void Update() + { + if (!PlayerControl.LocalPlayer) + return; + + if (!PlayerControl.LocalPlayer.IsCustomRole()) + return; + + if (CanUseKillButton != PlayerControl.LocalPlayer.GetCustomRole().CanKill()) + { + CanUseKillButton = !CanUseKillButton; + HudManager.Instance.SetHudActive(true); + } + } + + public override bool CanUse(IUsable usable) + { + var role = PlayerControl.LocalPlayer.GetCustomRole(); + if (role != null && role.CanVent) + { + CanVent = role.CanVent; + return usable.TryCast() != null; + } + + var console = usable.TryCast(); + + if (!role.HasToDoTasks) + return !(console != null) || console.AllowImpostor; + + return console != null; + } + + public override bool DidWin(GameOverReason gameOverReason) + { + /*var customRole = PlayerControl.LocalPlayer.GetCustomRole(); + if (customRole != null) + return customRole.DidWin(gameOverReason);*/ + PeasAPI.Logger.LogInfo(gameOverReason); + return false; + } + + public override void AppendTaskHint(StringBuilder taskStringBuilder) + { + if (BlurbMed.IsNullOrWhiteSpace()) + return; + + taskStringBuilder.AppendFormat("\n{0}{1} {2}\n{3}", NameColor.ToTextColor(), NiceName, + DestroyableSingleton.Instance.GetString(StringNames.RoleHint), BlurbMed); + } + + public override void SpawnTaskHeader(PlayerControl playerControl) + { + if (!playerControl.IsLocal()) + return; + + var importantTask = PlayerTask.GetOrCreateTask(playerControl); + importantTask.Text = string.Concat(NameColor.ToTextColor(), Blurb, "\r", + TasksCountTowardProgress + ? "" + : "\n" + DestroyableSingleton.Instance.GetString(StringNames.FakeTasks), + ""); + } + + public override PlayerControl FindClosestTarget() + { + if (PlayerControl.LocalPlayer.IsCustomRole()) + { + var playersInAbilityRangeSorted = GetPlayersInAbilityRangeSorted(GetTempPlayerList()); + if (playersInAbilityRangeSorted.Count <= 0) return null; + return playersInAbilityRangeSorted.ToArray()[0]; + } + + return null; + } +} \ No newline at end of file diff --git a/PeasAPI/Roles/Patches.cs b/PeasAPI/Roles/Patches.cs index 67f7062..22e90d9 100644 --- a/PeasAPI/Roles/Patches.cs +++ b/PeasAPI/Roles/Patches.cs @@ -1,13 +1,11 @@ -using System; -using System.Linq; +using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; using Il2CppSystem.Collections.Generic; using PeasAPI.CustomButtons; using PeasAPI.CustomRpc; -using Reactor.Networking; -using UnhollowerBaseLib; +using Reactor.Networking.Rpc; using UnityEngine; -using Object = Il2CppSystem.Object; namespace PeasAPI.Roles { @@ -27,7 +25,7 @@ public static void OnGameEndPatch(AmongUsClient __instance) [HarmonyPrefix] public static void ResetRolePatch(AmongUsClient __instance) { - PlayerControl.AllPlayerControls.ToArray().Where(player => player != null).Do(player => player.SetRole(null)); + PlayerControl.AllPlayerControls.ToArray().Where(player => player != null).Do(player => player.SetCustomRole(null)); } [HarmonyPatch(typeof(global::RoleManager), nameof(global::RoleManager.SelectRoles))] @@ -37,10 +35,10 @@ public static void InitializeRolesPatch() Rpc.Instance.Send(); } - [HarmonyPatch(typeof(global::RoleManager), nameof(global::RoleManager.AssignRolesFromList))] + [HarmonyPatch(typeof(global::LogicRoleSelectionNormal), nameof(global::LogicRoleSelectionNormal.AssignRolesFromList))] [HarmonyPrefix] - public static bool ChangeImpostors(global::RoleManager __instance, - [HarmonyArgument(0)] List players, [HarmonyArgument(1)] int teamMax, + public static bool ChangeImpostors(global::LogicRoleSelectionNormal __instance, + [HarmonyArgument(0)] List players, [HarmonyArgument(1)] int teamMax, [HarmonyArgument(2)] List roleList, [HarmonyArgument(3)] ref int rolesAssigned) { while (roleList.Count > 0 && players.Count > 0 && rolesAssigned < teamMax) @@ -59,13 +57,13 @@ public static bool ChangeImpostors(global::RoleManager __instance, return false; } - [HarmonyPatch(typeof(IntroCutscene._ShowRole_d__24), nameof(IntroCutscene._ShowRole_d__24.MoveNext))] + [HarmonyPatch(typeof(IntroCutscene._ShowRole_d__41), nameof(IntroCutscene._ShowRole_d__41.MoveNext))] [HarmonyPostfix] - public static void RoleTextPatch(IntroCutscene._ShowRole_d__24 __instance) + public static void RoleTextPatch(IntroCutscene._ShowRole_d__41 __instance) { - if (PlayerControl.LocalPlayer.GetRole() != null) + if (PlayerControl.LocalPlayer.GetCustomRole() != null) { - var role = PlayerControl.LocalPlayer.GetRole(); + var role = PlayerControl.LocalPlayer.GetCustomRole(); var scene = __instance.__4__this; scene.RoleText.text = role.Name; @@ -80,9 +78,9 @@ public static void RoleTextPatch(IntroCutscene._ShowRole_d__24 __instance) [HarmonyPostfix] public static void TeamTextPatch(IntroCutscene __instance) { - if (PlayerControl.LocalPlayer.GetRole() != null) + if (PlayerControl.LocalPlayer.GetCustomRole() != null) { - var role = PlayerControl.LocalPlayer.GetRole(); + var role = PlayerControl.LocalPlayer.GetCustomRole(); var scene = __instance; scene.TeamTitle.text = role.Name; @@ -99,9 +97,9 @@ public static void TeamTextPatch(IntroCutscene __instance) public static void RoleTeamPatch(IntroCutscene __instance, [HarmonyArgument(0)] ref List yourTeam) { - if (PlayerControl.LocalPlayer.GetRole() != null) + if (PlayerControl.LocalPlayer.GetCustomRole() != null) { - var role = PlayerControl.LocalPlayer.GetRole(); + var role = PlayerControl.LocalPlayer.GetCustomRole(); if (role.Team == Team.Alone) { yourTeam = new List(); @@ -151,18 +149,18 @@ public static bool Prefix(ref string __result, [HarmonyArgument(0)] StringNames return true; } }*/ - [HarmonyPatch(typeof(ExileController), nameof(ExileController.Begin))] + [HarmonyPatch(typeof(ExileController), nameof(ExileController.BeginForGameplay))] [HarmonyPostfix] - public static void ChangeExileTextPatch(ExileController __instance, [HarmonyArgument(0)] GameData.PlayerInfo exiled, [HarmonyArgument(1)] bool tie) + public static void ChangeExileTextPatch(ExileController __instance, [HarmonyArgument(0)] NetworkedPlayerInfo player, [HarmonyArgument(1)] bool voteTie) { - if (tie || exiled == null) + if (voteTie || player == null) return; - var role = exiled.Object.GetRole(); + var role = player.Object.GetCustomRole(); if (role != null) { var article = role.Members.Count > 1 ? "a" : "the"; - __instance.completeString = $"{ExileController.Instance.exiled.PlayerName} was {article} {role.Name}."; + __instance.completeString = $"{ExileController.Instance.initData.networkedPlayer.PlayerName} was {article} {role.Name}."; } } @@ -194,7 +192,7 @@ public static void Postfix(PlayerControl __instance) { if (PeasAPI.GameStarted) { - var localRole = PlayerControl.LocalPlayer.GetRole(); + var localRole = PlayerControl.LocalPlayer.GetCustomRole(); if (localRole != null && __instance.PlayerId == PlayerControl.LocalPlayer.PlayerId) { @@ -205,7 +203,7 @@ public static void Postfix(PlayerControl __instance) { if (!__instance.Data.Role.IsImpostor) __instance.SetKillTimer(__instance.killTimer - Time.fixedDeltaTime); - PlayerControl target = __instance.FindClosestTarget(false); + PlayerControl target = __instance.Data.Role.FindClosestTarget(); HudManager.Instance.KillButton.SetTarget(target); } else @@ -260,13 +258,13 @@ public static bool RemoveCheckMurder(KillButton __instance) Debug.LogWarning(string.Format("Bad kill from {0} to {1}", killer.PlayerId, num)); return false; } - GameData.PlayerInfo data = target.Data; + NetworkedPlayerInfo data = target.Data; if (data == null || data.IsDead || target.inVent) { Debug.LogWarning("Invalid target data for kill"); return false; } - PlayerControl.LocalPlayer.RpcMurderPlayer(__instance.currentTarget); + PlayerControl.LocalPlayer.RpcMurderPlayer(__instance.currentTarget, true); __instance.SetTarget(null); } return false; @@ -279,7 +277,7 @@ public static bool Prefix(KillButton __instance, [HarmonyArgument(0)] PlayerCont { if (!PlayerControl.LocalPlayer || PlayerControl.LocalPlayer.Data == null || !PlayerControl.LocalPlayer.Data.Role) return false; - RoleTeamTypes teamType = PlayerControl.LocalPlayer.GetRole() == null ? PlayerControl.LocalPlayer.Data.Role.TeamType : PlayerControl.LocalPlayer.GetRole().CanKill() ? RoleTeamTypes.Impostor : RoleTeamTypes.Crewmate; + RoleTeamTypes teamType = PlayerControl.LocalPlayer.GetCustomRole() == null ? PlayerControl.LocalPlayer.Data.Role.TeamType : PlayerControl.LocalPlayer.GetCustomRole().CanKill() ? RoleTeamTypes.Impostor : RoleTeamTypes.Crewmate; if (__instance.currentTarget && __instance.currentTarget != target) { __instance.currentTarget.ToggleHighlight(false, teamType); @@ -301,12 +299,12 @@ public static class PlayerControlSetKillTimerPatch { public static bool Prefix(PlayerControl __instance, [HarmonyArgument(0)] float time) { - if (__instance.GetRole() != null && __instance.GetRole().CanKill() || __instance.Data.Role.CanUseKillButton) + if (__instance.GetCustomRole() != null && __instance.GetCustomRole().CanKill() || __instance.Data.Role.CanUseKillButton) { - if (PlayerControl.GameOptions.KillCooldown <= 0f) + if (GameOptionsManager.Instance.currentNormalGameOptions.KillCooldown <= 0f) return false; - __instance.killTimer = Mathf.Clamp(time, 0f, PlayerControl.GameOptions.KillCooldown); - DestroyableSingleton.Instance.KillButton.SetCoolDown(__instance.killTimer, PlayerControl.GameOptions.KillCooldown); + __instance.killTimer = Mathf.Clamp(time, 0f, GameOptionsManager.Instance.currentNormalGameOptions.KillCooldown); + DestroyableSingleton.Instance.KillButton.SetCoolDown(__instance.killTimer, GameOptionsManager.Instance.currentNormalGameOptions.KillCooldown); } return false; } @@ -328,40 +326,23 @@ public static void OnPlayerExiledPatch(PlayerControl __instance) RoleManager.Roles.Do(r => r.OnExiled(__instance)); } - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.CoStartMeeting))] + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.StartMeeting))] [HarmonyPrefix] public static void OnMeetingStart(MeetingHud __instance) { RoleManager.Roles.Do(r => r.OnMeetingStart(__instance)); } - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.FindClosestTarget))] - public static class PlayerControlFindClosestTargetPatch - { - public static bool Prefix(PlayerControl __instance, out PlayerControl __result, - [HarmonyArgument(0)] bool protecting) - { - if (__instance.GetRole() != null) - { - __result = __instance.GetRole().FindClosestTarget(__instance, protecting); - return false; - } - - __result = null; - return true; - } - } - - [HarmonyPatch(typeof(PlayerControl._CoSetTasks_d__112), nameof(PlayerControl._CoSetTasks_d__112.MoveNext))] + [HarmonyPatch(typeof(PlayerControl._CoSetTasks_d__103), nameof(PlayerControl._CoSetTasks_d__103.MoveNext))] public static class PlayerControlSetTasks { - public static void Postfix(PlayerControl._CoSetTasks_d__112 __instance) + public static void Postfix(PlayerControl._CoSetTasks_d__103 __instance) { if (__instance == null) return; var player = __instance.__4__this; - var role = player.GetRole(); + var role = player.GetCustomRole(); if (role == null) return; @@ -397,25 +378,25 @@ public static bool Prefix(SabotageButton __instance) { if (__instance.isActiveAndEnabled && PeasAPI.GameStarted) { - var role = PlayerControl.LocalPlayer.GetRole(); - + var role = PlayerControl.LocalPlayer.GetCustomRole(); if (role == null) return true; - - HudManager.Instance.ShowMap((Action)(map => + + var mapOptions = new MapOptions { - foreach (MapRoom mapRoom in map.infectedOverlay.rooms.ToArray() - .Where(room => !role.CanSabotage(room.room))) - { - mapRoom.gameObject.SetActive(false); - } - - map.ShowSabotageMap(); - })); - + Mode = MapOptions.Modes.Sabotage + }; + + MapBehaviour.Instance.Show(mapOptions); + + foreach (MapRoom mapRoom in MapBehaviour.Instance.infectedOverlay.rooms.ToArray()) + { + mapRoom.gameObject.SetActive(role.CanSabotage(mapRoom.room)); + } + return false; } - + return true; } } @@ -427,18 +408,15 @@ public static bool Prefix(MapBehaviour __instance) { if (PeasAPI.GameStarted) { - var role = PlayerControl.LocalPlayer.GetRole(); + var role = PlayerControl.LocalPlayer.GetCustomRole(); if (role == null) return true; - HudManager.Instance.ShowMap((Action) (map => + foreach (MapRoom mapRoom in __instance.infectedOverlay.rooms.ToArray()) { - foreach (MapRoom mapRoom in map.infectedOverlay.rooms.ToArray()) - { - mapRoom.gameObject.SetActive(role.CanSabotage(mapRoom.room)); - } - })); + mapRoom.gameObject.SetActive(role.CanSabotage(mapRoom.room)); + } //return false; } @@ -454,7 +432,7 @@ public static class VentCanUsePatch public static void Postfix(Vent __instance, [HarmonyArgument(1)] ref bool canUse, [HarmonyArgument(2)] ref bool couldUse, ref float __result) { - BaseRole role = PlayerControl.LocalPlayer.GetRole(); + BaseRole role = PlayerControl.LocalPlayer.GetCustomRole(); if (role == null) return; @@ -475,9 +453,9 @@ public static void Postfix(Vent __instance, [HarmonyArgument(1)] ref bool canUse } } - [HarmonyPatch(typeof(ShipStatus), nameof(ShipStatus.RpcEndGame))] + [HarmonyPatch(typeof(GameManager), nameof(GameManager.RpcEndGame))] [HarmonyPrefix] - private static bool ShouldGameEndPatch(ShipStatus __instance, [HarmonyArgument(0)] GameOverReason endReason) + private static bool ShouldGameEndPatch(GameManager __instance, [HarmonyArgument(0)] GameOverReason endReason) { return RoleManager.Roles.Count(r => r.Members.Count != 0 && !r.ShouldGameEnd(endReason)) == 0; } @@ -520,7 +498,7 @@ private static bool DoTasksCountPatch(GameData __instance) __instance.CompletedTasks = 0; foreach (var playerInfo in __instance.AllPlayers) { - if (!playerInfo.Disconnected && playerInfo.Tasks != null && playerInfo.Object && (PlayerControl.GameOptions.GhostsDoTasks || !playerInfo.IsDead) && playerInfo.Role && playerInfo.Role.TasksCountTowardProgress && (playerInfo.GetRole() == null || playerInfo.GetRole().HasToDoTasks)) + if (!playerInfo.Disconnected && playerInfo.Tasks != null && playerInfo.Object && (GameOptionsManager.Instance.currentNormalGameOptions.GhostsDoTasks || !playerInfo.IsDead) && playerInfo.Role && playerInfo.Role.TasksCountTowardProgress && (playerInfo.GetCustomRole() == null || playerInfo.GetCustomRole().HasToDoTasks)) { foreach (var task in playerInfo.Tasks) { @@ -534,8 +512,8 @@ private static bool DoTasksCountPatch(GameData __instance) } /*for (int i = 0; i < __instance.AllPlayers.Count; i++) { - GameData.PlayerInfo playerInfo = __instance.AllPlayers[i]; - if (!playerInfo.Disconnected && playerInfo.Tasks != null && playerInfo.Object && (PlayerControl.GameOptions.GhostsDoTasks || !playerInfo.IsDead) && playerInfo.Role && playerInfo.Role.TasksCountTowardProgress) + NetworkedPlayerInfo playerInfo = __instance.AllPlayers[i]; + if (!playerInfo.Disconnected && playerInfo.Tasks != null && playerInfo.Object && (GameOptionsManager.Instance.currentNormalGameOptions.GhostsDoTasks || !playerInfo.IsDead) && playerInfo.Role && playerInfo.Role.TasksCountTowardProgress) { for (int j = 0; j < playerInfo.Tasks.Count; j++) { diff --git a/PeasAPI/Roles/RoleManager.cs b/PeasAPI/Roles/RoleManager.cs index 63cee14..e45a3a2 100644 --- a/PeasAPI/Roles/RoleManager.cs +++ b/PeasAPI/Roles/RoleManager.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; using System.Linq; +using AmongUs.GameOptions; using HarmonyLib; using PeasAPI.CustomRpc; -using Reactor; -using Reactor.Extensions; -using Reactor.Networking; +using Reactor.Localization.Utilities; +using Reactor.Networking.Rpc; +using Reactor.Utilities.Extensions; using UnityEngine; namespace PeasAPI.Roles @@ -25,23 +26,25 @@ internal static RoleBehaviour ToRoleBehaviour(BaseRole baseRole) { if (GameObject.Find($"{baseRole.Name}-Role")) { - return GameObject.Find($"{baseRole.Name}-Role").GetComponent(); + return GameObject.Find($"{baseRole.Name}-Role").GetComponent(); } var roleObject = new GameObject($"{baseRole.Name}-Role"); roleObject.DontDestroy(); - var role = roleObject.AddComponent(); - role.StringName = CustomStringName.Register(baseRole.Name); - role.BlurbName = CustomStringName.Register(baseRole.Description); - role.BlurbNameLong = CustomStringName.Register(baseRole.LongDescription); - role.BlurbNameMed = CustomStringName.Register(baseRole.Name); - role.Role = (RoleTypes) (6 + baseRole.Id); + var role = roleObject.AddComponent(); + role.StringName = CustomStringName.CreateAndRegister(baseRole.Name); + role.BlurbName = CustomStringName.CreateAndRegister(baseRole.Description); + role.BlurbNameLong = CustomStringName.CreateAndRegister(baseRole.LongDescription); + role.BlurbNameMed = CustomStringName.CreateAndRegister(baseRole.Name); + role.Role = (RoleTypes) (20 + baseRole.Id); + role.NameColor = baseRole.Color; var abilityButtonSettings = ScriptableObject.CreateInstance(); abilityButtonSettings.Image = baseRole.Icon; - abilityButtonSettings.Text = CustomStringName.Register(baseRole.Name); + abilityButtonSettings.Text = CustomStringName.CreateAndRegister(baseRole.Name); + abilityButtonSettings.FontMaterial = Material.GetDefaultMaterial(); role.Ability = abilityButtonSettings; role.TeamType = baseRole.Team switch @@ -57,8 +60,6 @@ internal static RoleBehaviour ToRoleBehaviour(BaseRole baseRole) role.CanVent = baseRole.CanVent; role.CanUseKillButton = baseRole.CanKill(); - PlayerControl.GameOptions.RoleOptions.SetRoleRate(role.Role, 0, 0); - global::RoleManager.Instance.AllRoles.AddItem(role); return role; diff --git a/PeasAPI/Utility.cs b/PeasAPI/Utility.cs index ee9a8af..c8cfb23 100644 --- a/PeasAPI/Utility.cs +++ b/PeasAPI/Utility.cs @@ -2,7 +2,8 @@ using System.IO; using System.Linq; using System.Reflection; -using Reactor.Extensions; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; using UnityEngine; namespace PeasAPI @@ -11,7 +12,7 @@ public static class Utility { public static Sprite CreateSprite(string image, float pixelsPerUnit = 128f) { - Texture2D tex = GUIExtensions.CreateEmptyTexture(); + Texture2D tex = CanvasUtilities.CreateEmptyTexture(); Stream myStream = Assembly.GetCallingAssembly().GetManifestResourceStream(image); byte[] data = myStream.ReadFully(); ImageConversion.LoadImage(tex, data, false); @@ -21,6 +22,18 @@ public static Sprite CreateSprite(string image, float pixelsPerUnit = 128f) return sprite; } + public static string ColorString(Color c, string s) + { + return string.Format("{4}", ToByte(c.r), ToByte(c.g), ToByte(c.b), + ToByte(c.a), s); + } + + private static byte ToByte(float f) + { + f = Mathf.Clamp01(f); + return (byte)(f * 255); + } + public static List GetAllPlayers() { if (PlayerControl.AllPlayerControls != null && PlayerControl.AllPlayerControls.Count > 0) diff --git a/build.cake b/build.cake deleted file mode 100644 index 56e3b47..0000000 --- a/build.cake +++ /dev/null @@ -1,32 +0,0 @@ -var target = Argument("target", "Build"); - -var buildId = EnvironmentVariable("GITHUB_RUN_NUMBER"); - -var @ref = EnvironmentVariable("GITHUB_REF"); -const string prefix = "refs/tags/"; -var tag = !string.IsNullOrEmpty(@ref) && @ref.StartsWith(prefix) ? @ref.Substring(prefix.Length) : null; - -Task("Build") - .Does(() => -{ - var settings = new DotNetCoreBuildSettings - { - Configuration = "Release", - MSBuildSettings = new DotNetCoreMSBuildSettings() - }; - - if (buildId != null) - { - settings.VersionSuffix = "ci." + buildId.Split("+")[0]; - } - - //settings.VersionSuffix = buildId.Split("+")[0] + "--helo--" + buildId.Split("+")[1]; - - foreach (var gamePlatform in new[] { "Steam", "Itch" }) - { - settings.MSBuildSettings.Properties["GamePlatform"] = new[] { gamePlatform }; - DotNetCoreBuild(".", settings); - } -}); - -RunTarget(target); diff --git a/PeasAPI/nuget.config b/nuget.config similarity index 56% rename from PeasAPI/nuget.config rename to nuget.config index 9a4ac66..b4401ac 100644 --- a/PeasAPI/nuget.config +++ b/nuget.config @@ -3,5 +3,7 @@ + + \ No newline at end of file From 2bc09f36bb6503851fab10b0626e1f86f0b5ebce Mon Sep 17 00:00:00 2001 From: FangkuaiYa <2683748223@qq.com> Date: Wed, 20 Aug 2025 19:13:07 +0800 Subject: [PATCH 2/4] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 373d7ec..3deace2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -37,4 +37,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: PeasAPI.nupkg - path: PeasAPI/bin/Release/PeasAPI.*.nupkg \ No newline at end of file + path: PeasAPI/bin/Release/PeasAPI-R.*.nupkg \ No newline at end of file From a0b4b1c1547f5cb4a3a665788ee606e4583c290c Mon Sep 17 00:00:00 2001 From: FangkuaiYa <2683748223@qq.com> Date: Sun, 24 Aug 2025 22:27:47 +0800 Subject: [PATCH 3/4] 1.9.1 Add CustomHatManager Add CustomVisorManager New Custom Option UI --- PeasAPI/CustomRpc/RpcInitializeRoles.cs | 26 +- PeasAPI/CustomRpc/RpcUpdateSetting.cs | 47 ++- PeasAPI/Data.json | 4 - PeasAPI/Extensions.cs | 2 + PeasAPI/Managers/CustomHatManager.cs | 316 +++++++++++++++++++ PeasAPI/Managers/CustomVisorManager.cs | 376 ++++++++++++++++++++++ PeasAPI/Managers/WatermarkManager.cs | 4 - PeasAPI/Options/CustomNumberOption.cs | 23 +- PeasAPI/Options/CustomOption.cs | 209 ++++++++++++- PeasAPI/Options/CustomOptionType.cs | 9 +- PeasAPI/Options/CustomRoleOption.cs | 72 ++++- PeasAPI/Options/Patches.cs | 397 ++++++++++++++---------- PeasAPI/PeasAPI-Icon.png | Bin 42709 -> 20060 bytes PeasAPI/PeasAPI.cs | 12 +- PeasAPI/PeasAPI.csproj | 10 +- PeasAPI/Roles/BaseRole.cs | 12 +- PeasAPI/Utility.cs | 50 ++- README.md | 36 ++- 18 files changed, 1337 insertions(+), 268 deletions(-) delete mode 100644 PeasAPI/Data.json create mode 100644 PeasAPI/Managers/CustomHatManager.cs create mode 100644 PeasAPI/Managers/CustomVisorManager.cs diff --git a/PeasAPI/CustomRpc/RpcInitializeRoles.cs b/PeasAPI/CustomRpc/RpcInitializeRoles.cs index 33b2390..cd55720 100644 --- a/PeasAPI/CustomRpc/RpcInitializeRoles.cs +++ b/PeasAPI/CustomRpc/RpcInitializeRoles.cs @@ -31,26 +31,26 @@ public override void Handle(PlayerControl innerNetObject) { var rolesForPlayers = new List(); - var roles = Roles.RoleManager.Roles.Where(role => role.Chance == 100).ToList(); + var roles = Roles.RoleManager.Roles.Where(role => role.GetChance() == 100).ToList(); foreach (var role in roles) { - for (int i = 0; i < role.Count; i++) + for (int i = 0; i < role.GetCount(); i++) { rolesForPlayers.Add(role); } } var roles2 = (from role in Roles.RoleManager.Roles - where role.Count > 0 && role.Chance > 0 && role.Chance < 100 - select role).ToList(); + where role.GetCount() > 0 && role.GetChance() > 0 && role.GetChance() < 100 + select role).ToList(); foreach (var role in roles2) { - for (int i = 0; i < role.Count; i++) + for (int i = 0; i < role.GetCount(); i++) { rolesForPlayers.Add(role); } } - + for (int i = 0; i < roles2.Count;) { var role = roles2.Random(); @@ -59,7 +59,7 @@ where role.Count > 0 && role.Chance > 0 && role.Chance < 100 temp.Remove(role); roles2 = temp; } - + rolesForPlayers.Do(AssignRole); } } @@ -88,7 +88,7 @@ private void AssignRole(BaseRole role) id.GetPlayer().Data.Role.IsSimpleRole && !RoleManager.IsGhostRole(id.GetPlayerInfo().Role.Role) && id.GetPlayer().GetCustomRole() == null) .ToArray(); - + if (nonRoleImpostors.Length == 0) return; @@ -100,7 +100,7 @@ private void AssignRole(BaseRole role) } var chance = HashRandom.Next(101); - if (chance < role.Chance) + if (chance < role.GetChance()) { var member = nonRoleImpostors[PeasAPI.Random.Next(0, nonRoleImpostors.Length)]; @@ -113,19 +113,19 @@ private void AssignRole(BaseRole role) id.GetPlayer().Data.Role.IsSimpleRole && !RoleManager.IsGhostRole(id.GetPlayerInfo().Role.Role) && id.GetPlayer().GetCustomRole() == null) .ToArray(); - + if (nonRoleCrewmates.Length == 0) return; - + if (Roles.RoleManager.HostMod.IsRole.ContainsKey(role) && Roles.RoleManager.HostMod.IsRole[role] && PlayerControl.LocalPlayer.GetCustomRole() == null) { PlayerControl.LocalPlayer.RpcSetRole(role); return; } - + var chance = HashRandom.Next(101); - if (chance < role.Chance) + if (chance < role.GetChance()) { var member = nonRoleCrewmates[PeasAPI.Random.Next(0, nonRoleCrewmates.Length)]; diff --git a/PeasAPI/CustomRpc/RpcUpdateSetting.cs b/PeasAPI/CustomRpc/RpcUpdateSetting.cs index 1304a17..2985b2d 100644 --- a/PeasAPI/CustomRpc/RpcUpdateSetting.cs +++ b/PeasAPI/CustomRpc/RpcUpdateSetting.cs @@ -43,32 +43,26 @@ public static IEnumerator SendRpc(CustomOption optionn = null, int RecipientId = writer.Write((bool)option.ValueObject); break; case CustomOptionType.Number: - { - switch (option.CustomRoleOptionType) { - case CustomRoleOptionType.None: - switch ((option as CustomNumberOption).IntSafe) - { - case true: - writer.WritePacked((int)(float)option.ValueObject); - break; - case false: - writer.Write((float)option.ValueObject); - break; - } + switch ((option as CustomNumberOption).IntSafe) + { + case true: + writer.WritePacked((int)(float)option.ValueObject); + break; + case false: + writer.Write((float)option.ValueObject); + break; + } - break; - case CustomRoleOptionType.Chance: - writer.Write(Convert.ToInt32(option.ValueObject)); - option.BaseRole.Chance = Convert.ToInt32(option.ValueObject); - break; - case CustomRoleOptionType.Count: - writer.Write(Convert.ToInt32(option.ValueObject)); - option.BaseRole.Count = - option.BaseRole.MaxCount = Convert.ToInt32(option.ValueObject); - break; } - } + break; + case CustomOptionType.Role: + { + writer.WritePacked(Convert.ToInt32(option.ValueObject)); + option.BaseRole.Chance = Convert.ToInt32(option.ValueObject); + writer.WritePacked(Convert.ToInt32(option.ValueObject2)); + option.BaseRole.Count = option.BaseRole.MaxCount = Convert.ToInt32(option.ValueObject2); + } break; case CustomOptionType.String: writer.WritePacked((int)option.ValueObject); @@ -91,6 +85,7 @@ public static void ReceiveRpc(MessageReader reader, bool AllOptions) option.ID == id); // Works but may need to change to gameObject.name check var type = customOption?.Type; object value = null; + object value2 = null; switch (type) { @@ -108,13 +103,17 @@ public static void ReceiveRpc(MessageReader reader, bool AllOptions) break; } + break; + case CustomOptionType.Role: + value = reader.ReadPackedInt32(); + value2 = reader.ReadPackedInt32(); break; case CustomOptionType.String: value = reader.ReadPackedInt32(); break; } - customOption?.Set(value, Notify: !AllOptions); + customOption?.Set(value, value2, Notify: !AllOptions); if (LobbyInfoPane.Instance.LobbyViewSettingsPane.gameObject.activeSelf) { diff --git a/PeasAPI/Data.json b/PeasAPI/Data.json deleted file mode 100644 index 8539a3c..0000000 --- a/PeasAPI/Data.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "version": "1.7.0", - "downloadUrl": "https://github.com/Peasplayer/PeasAPI/releases/download/1.7.0/PeasAPI.dll" -} diff --git a/PeasAPI/Extensions.cs b/PeasAPI/Extensions.cs index 0a6188e..c16e03e 100644 --- a/PeasAPI/Extensions.cs +++ b/PeasAPI/Extensions.cs @@ -92,6 +92,8 @@ public static string GetTranslation(this StringNames stringName) /// public static BaseRole GetCustomRole(this PlayerControl player) { + if (Roles.RoleManager.Roles == null) return null; + foreach (var _role in Roles.RoleManager.Roles) { if (_role.Members.Contains(player.PlayerId)) diff --git a/PeasAPI/Managers/CustomHatManager.cs b/PeasAPI/Managers/CustomHatManager.cs new file mode 100644 index 0000000..a8171c6 --- /dev/null +++ b/PeasAPI/Managers/CustomHatManager.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using AmongUs.Data; +using BepInEx.Logging; +using HarmonyLib; +using PowerTools; +using Reactor.Utilities; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; +using Object = UnityEngine.Object; + +namespace PeasAPI.Managers; + +public static class CustomHatManager +{ + private static bool LoadedHats; + internal static readonly Dictionary ViewDataCache = new(); + internal static readonly List RegisteredHats = new(); + + private static ManualLogSource Log => PluginSingleton.Instance.Log; + + internal static void LoadHatsRoutine() + { + if (LoadedHats || !DestroyableSingleton.InstanceExists || + DestroyableSingleton.Instance.allHats.Count == 0) + return; + LoadedHats = true; + Coroutines.Start(LoadHats()); + } + + internal static IEnumerator LoadHats() + { + try + { + var hatData = new List(); + hatData.AddRange(DestroyableSingleton.Instance.allHats); + hatData.ForEach(x => x.StoreName = "Vanilla"); + + var originalCount = DestroyableSingleton.Instance.allHats.ToList().Count; + + // Process registered hats + for (var i = 0; i < RegisteredHats.Count; i++) + { + var customHat = RegisteredHats[i]; + var hatBehaviour = GenerateHatBehaviour(customHat); + hatBehaviour.StoreName = customHat.ModName; + hatBehaviour.ProductId = customHat.Name; + hatBehaviour.name = customHat.Name + $"({customHat.Author})"; + hatBehaviour.Free = true; + hatBehaviour.displayOrder = originalCount + i; + hatData.Add(hatBehaviour); + } + + DestroyableSingleton.Instance.allHats = hatData.ToArray(); + } + catch (Exception e) + { + Log.LogError($"Error while loading hats: {e.Message}\nStack: {e.StackTrace}"); + } + + yield return null; + } + + public static void RegisterNewHat(string name, Sprite image, Vector2 chipOffset = default, + bool inFront = true, bool noBounce = true, string author = "Unknown", string modName = "Custom", Sprite climbImage = null, + Sprite backImage = null, Sprite floorImage = null) + { + if (chipOffset == default) + chipOffset = new Vector2(-0.1f, 0.35f); + + RegisteredHats.Add(new CustomHat + { + Name = name, + Image = image, + ChipOffset = chipOffset, + InFront = inFront, + NoBounce = noBounce, + Author = author, + ModName = modName, + ClimbImage = climbImage, + BackImage = backImage, + FloorImage = floorImage + }); + } + + private static HatData GenerateHatBehaviour(CustomHat customHat) + { + Sprite sprite; + if (HatCache.hatViewDatas.ContainsKey(customHat.Name)) + { + sprite = HatCache.hatViewDatas[customHat.Name]; + } + else + { + sprite = customHat.Image; + if (sprite != null) + { + HatCache.hatViewDatas.Add(customHat.Name, sprite); + } + else + { + Log.LogError($"Failed to load hat image: {customHat.Image}"); + return null; + } + } + + var hat = ScriptableObject.CreateInstance(); + var viewData = ViewDataCache[hat.name] = ScriptableObject.CreateInstance(); + + hat.ChipOffset = customHat.ChipOffset; + viewData.MainImage = sprite; + viewData.ClimbImage = customHat.ClimbImage ?? sprite; + viewData.BackImage = customHat.BackImage ?? sprite; + viewData.FloorImage = customHat.FloorImage ?? sprite; + hat.ViewDataRef = new AssetReference(ViewDataCache[hat.name].Pointer); + hat.InFront = customHat.InFront; + hat.NoBounce = customHat.NoBounce; + + return hat; + } + + [HarmonyPatch(typeof(AmongUsClient), nameof(AmongUsClient.Awake))] + public class AmongUsClient_Patches + { + private static bool _executed; + + public static void Prefix() + { + if (!_executed) + { + LoadHats(); + _executed = true; + } + } + } + + [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetHat), typeof(int))] + public static class HP_patch + { + public static bool Prefix(HatParent __instance, int color) + { + if (!HatCache.hatViewDatas.ContainsKey(__instance.Hat.ProductId)) return true; + __instance.UnloadAsset(); + __instance.PopulateFromViewData(); + __instance.SetMaterialColor(color); + return false; + } + } + + [HarmonyPatch(typeof(HatParent), nameof(HatParent.PopulateFromViewData))] + public static class PF_patch + { + public static bool Prefix(HatParent __instance) + { + if (!HatCache.hatViewDatas.ContainsKey(__instance.Hat.ProductId)) return true; + __instance.UpdateMaterial(); + var hat = HatCache.hatViewDatas[__instance.Hat.ProductId]; + + var spriteAnimNodeSync = __instance.SpriteSyncNode ?? __instance.GetComponent(); + if ((bool)spriteAnimNodeSync) + spriteAnimNodeSync.NodeId = __instance.Hat.NoBounce ? 1 : 0; + if (__instance.Hat.InFront) + { + __instance.BackLayer.enabled = false; + __instance.FrontLayer.enabled = true; + __instance.FrontLayer.sprite = hat; + } + else + { + __instance.BackLayer.enabled = true; + __instance.FrontLayer.enabled = false; + __instance.FrontLayer.sprite = null; + __instance.BackLayer.sprite = hat; + } + + if (!__instance.options.Initialized || !__instance.HideHat()) + return false; + __instance.FrontLayer.enabled = false; + __instance.BackLayer.enabled = false; + return false; + } + } + + [HarmonyPatch(typeof(HatParent), nameof(HatParent.SetClimbAnim))] + public static class PF_climb_patch + { + public static bool Prefix(HatParent __instance) + { + if (!HatCache.hatViewDatas.ContainsKey(__instance.Hat.ProductId)) return true; + __instance.FrontLayer.sprite = null; + return false; + } + } + + [HarmonyPatch(typeof(InventoryManager), nameof(InventoryManager.CheckUnlockedItems))] + public class InventoryManager_Patches + { + public static void Prefix() + { + LoadHatsRoutine(); + } + } + + [HarmonyPatch(typeof(HatsTab), nameof(HatsTab.OnEnable))] + public static class HatsTab_OnEnable + { + public static bool Prefix(HatsTab __instance) + { + __instance.currentHat = + DestroyableSingleton.Instance.GetHatById(DataManager.Player.Customization.Hat); + var allHats = DestroyableSingleton.Instance.GetUnlockedHats(); + var hatGroups = new SortedList>( + new PaddedComparer("Vanilla", "") + ); + foreach (var hat in allHats) + { + if (!hatGroups.ContainsKey(hat.StoreName)) + hatGroups[hat.StoreName] = new List(); + hatGroups[hat.StoreName].Add(hat); + } + + foreach (var instanceColorChip in __instance.ColorChips) + instanceColorChip.gameObject.Destroy(); + __instance.ColorChips.Clear(); + var groupNameText = __instance.GetComponentInChildren(false); + var hatIdx = 0; + foreach (var (groupName, hats) in hatGroups) + { + var text = Object.Instantiate(groupNameText, __instance.scroller.Inner); + text.gameObject.transform.localScale = Vector3.one; + text.GetComponent().Destroy(); + text.text = groupName; + text.alignment = TextAlignmentOptions.Center; + text.fontSize = 3f; + text.fontSizeMax = 3f; + text.fontSizeMin = 0f; + + hatIdx = (hatIdx + 4) / 5 * 5; + + var xLerp = __instance.XRange.Lerp(0.5f); + var yLerp = __instance.YStart - hatIdx / __instance.NumPerRow * __instance.YOffset; + text.transform.localPosition = new Vector3(xLerp, yLerp, -1f); + hatIdx += 5; + foreach (var hat in hats.OrderBy(HatManager.Instance.allHats.IndexOf)) + { + var num = __instance.XRange.Lerp(hatIdx % __instance.NumPerRow / (__instance.NumPerRow - 1f)); + var num2 = __instance.YStart - hatIdx / __instance.NumPerRow * __instance.YOffset; + var colorChip = Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner); + colorChip.transform.localPosition = new Vector3(num, num2, -1f); + colorChip.Button.OnClick.AddListener((Action)(() => __instance.SelectHat(hat))); + colorChip.Inner.SetHat(hat, + __instance.HasLocalPlayer() + ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId + : DataManager.Player.Customization.Color); + colorChip.Inner.transform.localPosition = hat.ChipOffset + new Vector2(0f, -0.3f); + colorChip.Tag = hat; + __instance.ColorChips.Add(colorChip); + hatIdx += 1; + } + } + + __instance.scroller.ContentYBounds.max = + -(__instance.YStart - (hatIdx + 1) / __instance.NumPerRow * __instance.YOffset) - 3f; + __instance.currentHatIsEquipped = true; + + return false; + } + } + + public static class HatCache + { + public static Dictionary hatViewDatas = new(); + } + + public class PaddedComparer : IComparer where T : IComparable + { + private readonly T[] _forcedToBottom; + + public PaddedComparer(params T[] forcedToBottom) + { + _forcedToBottom = forcedToBottom; + } + + public int Compare(T x, T y) + { + if (_forcedToBottom.Contains(x) && _forcedToBottom.Contains(y)) + return StringComparer.InvariantCulture.Compare(x, y); + + if (_forcedToBottom.Contains(x)) + return 1; + if (_forcedToBottom.Contains(y)) + return -1; + + return StringComparer.InvariantCulture.Compare(x, y); + } + } +} + +// New class to store custom hat data +public class CustomHat +{ + public string Name { get; set; } + public Sprite Image { get; set; } + public Vector2 ChipOffset { get; set; } + public bool InFront { get; set; } + public bool NoBounce { get; set; } + public string Author { get; set; } + public string ModName { get; set; } + public Sprite ClimbImage { get; set; } + public Sprite BackImage { get; set; } + public Sprite FloorImage { get; set; } +} diff --git a/PeasAPI/Managers/CustomVisorManager.cs b/PeasAPI/Managers/CustomVisorManager.cs new file mode 100644 index 0000000..9798450 --- /dev/null +++ b/PeasAPI/Managers/CustomVisorManager.cs @@ -0,0 +1,376 @@ +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; +using System.Linq; +using System; +using UnityEngine.AddressableAssets; +using AmongUs.Data; +using Innersloth.Assets; +using Reactor.Utilities.Extensions; +using Reactor.Utilities; +using TMPro; + +namespace PeasAPI.Managers +{ + public static class CustomVisorManager + { + public static Material MagicShader = new Material(Shader.Find("Unlit/PlayerShader")); + + public struct CustomVisorData + { + public string Name; + public string Author; + public Sprite Image; + public Sprite ClimbImage; + public Sprite FloorImage; + public Vector2 ChipOffset; + public bool MatchPlayerColor; + public string Group; + } + + public static bool _customVisorLoaded = false; + static readonly List visorData = new(); + private static readonly List customVisorData = new(); + public static readonly Dictionary CustomVisorViewDatas = []; + public static readonly Dictionary> RegisteredVisors = new(); + + public static void RegisterNewVisor(string name, Sprite image, Vector2 chipOffset = new Vector2(), + Sprite climbImage = null, Sprite floorImage = null, + bool matchPlayerColor = false, string author = "Unknown", string group = "Custom") + { + if (!RegisteredVisors.ContainsKey(group)) + { + RegisteredVisors[group] = new List(); + } + + RegisteredVisors[group].Add(new CustomVisorData + { + Name = name, + Author = author, + Image = image, + ClimbImage = climbImage, + FloorImage = floorImage, + ChipOffset = chipOffset, + MatchPlayerColor = matchPlayerColor, + Group = group + }); + } + + [HarmonyPatch(typeof(HatManager), nameof(HatManager.GetVisorById))] + class AddCustomVisorsPatch + { + public static void Postfix(HatManager __instance) + { + if (_customVisorLoaded) return; + _customVisorLoaded = true; + var AllVisors = __instance.allVisors.ToList(); + + foreach (var group in RegisteredVisors) + { + foreach (var data in group.Value) + { + VisorViewData vvd = new VisorViewData(); + vvd.IdleFrame = data.Image; + vvd.LeftIdleFrame = data.ClimbImage; + vvd.FloorFrame = data.FloorImage; + vvd.MatchPlayerColor = data.MatchPlayerColor; + + var visor = new CustomVisors(vvd); + visor.name = $"{data.Name} (by {data.Author})"; + visor.ProductId = "lmj_" + visor.name.Replace(' ', '_'); + visor.BundleId = "lmj_" + visor.name.Replace(' ', '_'); + visor.displayOrder = 99; + visor.ChipOffset = data.ChipOffset; + visor.Free = true; + visorData.Add(visor); + customVisorData.Add(visor); + var assetRef = new AssetReference(vvd.Pointer); + visor.ViewDataRef = assetRef; + visor.CreateAddressableAsset(); + CustomVisorViewDatas.TryAdd(visor.ProductId, vvd); + } + } + + AllVisors.AddRange(visorData); + __instance.allVisors = AllVisors.ToArray(); + } + } + + [HarmonyPatch(typeof(VisorsTab), nameof(VisorsTab.OnEnable))] + public static class VisorsTabOnEnablePatch + { + private static TMP_Text Template; + + private static float CreateVisorPackage(List visors, string packageName, float YStart, VisorsTab __instance) + { + + var offset = YStart; + + if (Template) + { + var title = UnityEngine.Object.Instantiate(Template, __instance.scroller.Inner); + var material = title.GetComponent().material; + material.SetFloat("_StencilComp", 4f); + material.SetFloat("_Stencil", 1f); + title.transform.localPosition = new(2.25f, YStart, -1f); + title.transform.localScale = Vector3.one * 1.5f; + title.fontSize *= 0.5f; + title.enableAutoSizing = false; + Coroutines.Start(Utility.PerformTimedAction(0.1f, _ => title.SetText(packageName, true))); + offset -= 0.8f * __instance.YOffset; + } + + for (var i = 0; i < visors.Count; i++) + { + var visor = visors[i]; + var xpos = __instance.XRange.Lerp(i % __instance.NumPerRow / (__instance.NumPerRow - 1f)); + var ypos = offset - (i / __instance.NumPerRow * __instance.YOffset); + var colorChip = UnityEngine.Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner); + + if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard) + { + colorChip.Button.OverrideOnMouseOverListeners(() => __instance.SelectVisor(visor)); + colorChip.Button.OverrideOnMouseOutListeners(() => __instance.SelectVisor(HatManager.Instance.GetVisorById(DataManager.Player.Customization.Visor))); + colorChip.Button.OverrideOnClickListeners(__instance.ClickEquip); + } + else + colorChip.Button.OverrideOnClickListeners(() => __instance.SelectVisor(visor)); + + colorChip.Button.ClickMask = __instance.scroller.Hitbox; + colorChip.transform.localPosition = new(xpos, ypos, -1f); + colorChip.Inner.SetMaskType(PlayerMaterial.MaskType.SimpleUI); + colorChip.Inner.transform.localPosition = visor.ChipOffset; + colorChip.ProductId = visor.ProductId; + colorChip.Tag = visor; + __instance.UpdateMaterials(colorChip.Inner.FrontLayer, visor); + var colorId = __instance.HasLocalPlayer() ? PlayerControl.LocalPlayer.Data.DefaultOutfit.ColorId : DataManager.Player.Customization.Color; + + if (CustomVisorViewDatas.TryGetValue(visor.ProductId, out var data)) + ColorChipFix(colorChip, data.IdleFrame, colorId); + else + visor.SetPreview(colorChip.Inner.FrontLayer, colorId); + + colorChip.SelectionHighlight.gameObject.SetActive(false); + __instance.ColorChips.Add(colorChip); + } + + return offset - ((visors.Count - 1) / __instance.NumPerRow * __instance.YOffset) - 1.5f; + } + + private static void ColorChipFix(ColorChip chip, Sprite sprite, int colorId) + { + chip.Inner.FrontLayer.sprite = sprite; + AddressableAssetHandler.AddToGameObject(chip.Inner.FrontLayer.gameObject); + + if (Application.isPlaying) + PlayerMaterial.SetColors(colorId, chip.Inner.FrontLayer); + } + + public static bool Prefix(VisorsTab __instance) + { + for (var i = 0; i < __instance.scroller.Inner.childCount; i++) + __instance.scroller.Inner.GetChild(i).gameObject.Destroy(); + + __instance.ColorChips = new(); + var array = HatManager.Instance.GetUnlockedVisors(); + var packages = new Dictionary>(); + + foreach (var data in array) + { + var package = "Innersloth"; + + if (data.ProductId.StartsWith("lmj_")) + { + package = "Custom"; + foreach (var group in RegisteredVisors) + { + if (customVisorData.Any(v => v.ProductId == data.ProductId && + group.Value.Any(vd => $"{vd.Name} (by {vd.Author})" == v.name))) + { + package = group.Key; + break; + } + } + } + + if (!packages.ContainsKey(package)) + packages[package] = []; + + packages[package].Add(data); + } + + var yOffset = __instance.YStart; + Template = __instance.transform.FindChild("Text").gameObject.GetComponent(); + var keys = packages.Keys.OrderBy(x => x switch { + "Innersloth" => 999, + _ => Array.IndexOf(RegisteredVisors.Keys.ToArray(), x) + }); + + keys.ForEach(key => yOffset = CreateVisorPackage(packages[key], key, yOffset, __instance)); + + if (array.Length != 0) + __instance.GetDefaultSelectable().PlayerEquippedForeground.SetActive(true); + + __instance.visorId = DataManager.Player.Customization.Visor; + __instance.currentVisorIsEquipped = true; + __instance.SetScrollerBounds(); + __instance.scroller.ContentYBounds.max = -(yOffset + 4.1f); + return false; + } + } + + [HarmonyPatch(typeof(CosmeticsCache), nameof(CosmeticsCache.GetVisor))] + class CosmeticsCacheGetVisorPatch + { + public static bool Prefix(CosmeticsCache __instance, string id, ref VisorViewData __result) + { + if (!id.StartsWith("lmj_")) return true; + __result = GetByCache(id); + if (__result == null) + __result = __instance.visors["visor_EmptyVisor"].GetAsset(); + return false; + } + } + + static Dictionary cache = new(); + static VisorViewData GetByCache(string id) + { + if (!cache.ContainsKey(id)) + { + cache[id] = customVisorData.FirstOrDefault(x => x.ProductId == id)?.visorViewData; + } + return cache[id]; + } + + [HarmonyPatch(typeof(VisorLayer), nameof(VisorLayer.UpdateMaterial))] + class VisorLayerUpdateMaterialPatch + { + public static bool Prefix(VisorLayer __instance) + { + if (__instance.visorData == null || !__instance.visorData.ProductId.StartsWith("lmj_")) return true; + VisorViewData asset = GetByCache(__instance.visorData.ProductId); + if (asset == null) return true; + + PlayerMaterial.MaskType maskType = __instance.matProperties.MaskType; + if (asset.MatchPlayerColor) + { + if (maskType == PlayerMaterial.MaskType.ComplexUI || maskType == PlayerMaterial.MaskType.ScrollingUI) + { + __instance.Image.sharedMaterial = DestroyableSingleton.Instance.MaskedPlayerMaterial; + } + else + { + __instance.Image.sharedMaterial = DestroyableSingleton.Instance.PlayerMaterial; + } + } + else if (maskType == PlayerMaterial.MaskType.ComplexUI || maskType == PlayerMaterial.MaskType.ScrollingUI) + { + __instance.Image.sharedMaterial = DestroyableSingleton.Instance.MaskedMaterial; + } + else + { + __instance.Image.sharedMaterial = DestroyableSingleton.Instance.DefaultShader; + } + switch (maskType) + { + case PlayerMaterial.MaskType.SimpleUI: + __instance.Image.maskInteraction = (SpriteMaskInteraction)1; + break; + case PlayerMaterial.MaskType.Exile: + __instance.Image.maskInteraction = (SpriteMaskInteraction)2; + break; + default: + __instance.Image.maskInteraction = (SpriteMaskInteraction)0; + break; + } + __instance.Image.material.SetInt(PlayerMaterial.MaskLayer, __instance.matProperties.MaskLayer); + if (asset.MatchPlayerColor) + { + PlayerMaterial.SetColors(__instance.matProperties.ColorId, __instance.Image); + } + if (__instance.matProperties.MaskLayer <= 0) + { + PlayerMaterial.SetMaskLayerBasedOnLocalPlayer(__instance.Image, __instance.matProperties.IsLocalPlayer); + return false; + } + __instance.Image.material.SetInt(PlayerMaterial.MaskLayer, __instance.matProperties.MaskLayer); + return false; + } + } + + [HarmonyPatch(typeof(VisorLayer), nameof(VisorLayer.SetFlipX))] + class VisorLayerSetFlipXPatch + { + public static bool Prefix(VisorLayer __instance, bool flipX) + { + if (__instance.visorData == null || !__instance.visorData.ProductId.StartsWith("lmj_")) return true; + VisorViewData asset = GetByCache(__instance.visorData.ProductId); + if (asset == null) return true; + + __instance.Image.flipX = flipX; + if (flipX && asset.LeftIdleFrame) + { + __instance.Image.sprite = asset.LeftIdleFrame; + } + else + { + __instance.Image.sprite = asset.IdleFrame; + } + return false; + } + } + + [HarmonyPatch(typeof(VisorLayer), nameof(VisorLayer.SetFloorAnim))] + class VisorLayerSetVisorFloorPositionPatch + { + public static bool Prefix(VisorLayer __instance) + { + if (__instance.visorData == null || !__instance.visorData.ProductId.StartsWith("lmj_")) return true; + VisorViewData asset = GetByCache(__instance.visorData.ProductId); + if (asset == null) return true; + + __instance.Image.sprite = asset.FloorFrame ? asset.FloorFrame : asset.IdleFrame; + return false; + } + } + + [HarmonyPatch(typeof(VisorLayer), nameof(VisorLayer.PopulateFromViewData))] + class VisorLayerPopulateFromViewDataPatch + { + public static bool Prefix(VisorLayer __instance) + { + if (__instance.visorData == null || !__instance.visorData.ProductId.StartsWith("lmj_")) + return true; + __instance.UpdateMaterial(); + if (!__instance.IsDestroyedOrNull()) + { + __instance.transform.SetLocalZ(__instance.DesiredLocalZPosition); + __instance.SetFlipX(__instance.Image.flipX); + } + return false; + } + } + + [HarmonyPatch(typeof(VisorLayer), nameof(VisorLayer.SetVisor), new Type[] { typeof(VisorData), typeof(int) })] + class VisorLayerSetVisorPatch + { + public static bool Prefix(VisorLayer __instance, VisorData data, int color) + { + if (!data.ProductId.StartsWith("lmj_")) return true; + __instance.visorData = data; + __instance.SetMaterialColor(color); + __instance.PopulateFromViewData(); + return false; + } + } + } + + class CustomVisors : VisorData + { + public VisorViewData visorViewData; + public CustomVisors(VisorViewData hvd) + { + visorViewData = hvd; + } + } +} \ No newline at end of file diff --git a/PeasAPI/Managers/WatermarkManager.cs b/PeasAPI/Managers/WatermarkManager.cs index 3c70cfc..4d5ef62 100644 --- a/PeasAPI/Managers/WatermarkManager.cs +++ b/PeasAPI/Managers/WatermarkManager.cs @@ -1,13 +1,9 @@ using System.Collections.Generic; -using BepInEx; using HarmonyLib; -using Il2CppInterop.Runtime; using InnerNet; -using Reactor; using Reactor.Patches; using TMPro; using UnityEngine; -using static PeasAPI.Managers.WatermarkManager; namespace PeasAPI.Managers { diff --git a/PeasAPI/Options/CustomNumberOption.cs b/PeasAPI/Options/CustomNumberOption.cs index c0a6775..8b30d79 100644 --- a/PeasAPI/Options/CustomNumberOption.cs +++ b/PeasAPI/Options/CustomNumberOption.cs @@ -1,17 +1,12 @@ using System; -using PeasAPI.Roles; -using UnityEngine; namespace PeasAPI.Options; public class CustomNumberOption : CustomOption { - public CustomNumberOption(MultiMenu multiMenu, string optionName, float value, - float increment, float min, float max, - Func format = null, CustomRoleOptionType customRoleOptionType = CustomRoleOptionType.None, - BaseRole baseRole = null) - : base(num++, multiMenu, optionName, CustomOptionType.Number, value, format, customRoleOptionType, - baseRole) + public CustomNumberOption(MultiMenu multiMenu, string optionName, float min, float max, + float increment, float value, Func format = null) + : base(num++, multiMenu, optionName, CustomOptionType.Number, value, null, format) { Min = min; Max = max; @@ -28,23 +23,19 @@ public CustomNumberOption(MultiMenu multiMenu, string optionName, float value, public void Increase() { - var increment = Increment > 5 && Input.GetKeyInt(KeyCode.LeftShift) ? 5 : Increment; - - if (Value + increment > + if (Value + Increment > Max + 0.001f) // the slight increase is because of the stupid float rounding errors in the Giant speed Set(Min); else - Set(Value + increment); + Set(Value + Increment); } public void Decrease() { - var increment = Increment > 5 && Input.GetKeyInt(KeyCode.LeftShift) ? 5 : Increment; - - if (Value - increment < Min - 0.001f) // added it here to in case I missed something else + if (Value - Increment < Min - 0.001f) // added it here to in case I missed something else Set(Max); else - Set(Value - increment); + Set(Value - Increment); } public override void OptionCreated() diff --git a/PeasAPI/Options/CustomOption.cs b/PeasAPI/Options/CustomOption.cs index 2cd467d..917c572 100644 --- a/PeasAPI/Options/CustomOption.cs +++ b/PeasAPI/Options/CustomOption.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using PeasAPI.CustomRpc; using PeasAPI.Roles; using Reactor.Localization.Utilities; @@ -11,13 +12,12 @@ public class CustomOption { public static List AllOptions = new(); + private static bool _isLoading = false; public static int num = 1; public readonly int ID; public readonly MultiMenu Menu; public BaseRole BaseRole; - public CustomRoleOptionType CustomRoleOptionType; - public Func Format; public string Name; public bool IsRoleOption; @@ -25,8 +25,8 @@ public class CustomOption public StringNames StringName; public CustomOption(int id, MultiMenu menu, string name, CustomOptionType type, - object defaultValue, - Func format = null, CustomRoleOptionType customRoleOptionType = CustomRoleOptionType.None, + object defaultValue, object defaultValue2 = null, + Func format = null, BaseRole baseRole = null, bool isRoleOption = false) { ID = id; @@ -34,29 +34,26 @@ public CustomOption(int id, MultiMenu menu, string name, CustomOptionType type, Name = name; Type = type; DefaultValue = ValueObject = defaultValue; + DefaultValue2 = ValueObject2 = defaultValue2; Format = format ?? (obj => $"{obj}"); BaseRole = baseRole; - CustomRoleOptionType = customRoleOptionType; - if (Type == CustomOptionType.Button) return; AllOptions.Add(this); - Set(ValueObject); + Set(ValueObject, ValueObject2); - StringName = CustomStringName.CreateAndRegister( - customRoleOptionType == CustomRoleOptionType.Chance || customRoleOptionType == CustomRoleOptionType.Count - ? Utility.ColorString(baseRole.Color, baseRole.Name) + $" {GetName()}" - : GetName()); + StringName = CustomStringName.CreateAndRegister(GetName()); IsRoleOption = isRoleOption; } public object ValueObject { get; set; } + public object ValueObject2 { get; set; } public OptionBehaviour Setting { get; set; } public CustomOptionType Type { get; set; } public object DefaultValue { get; set; } + public object DefaultValue2 { get; set; } public static Func Seconds { get; } = value => $"{value:0.0#}s"; public static Func Multiplier { get; } = value => $"{value:0.0#}x"; - public string GetName() { return Name; @@ -67,16 +64,178 @@ public override string ToString() return Format(ValueObject); } + public string ToString2() + { + return Format(ValueObject2); + } + public virtual void OptionCreated() { Setting.name = Setting.gameObject.name = GetName(); } - public void Set(object value, bool SendRpc = true, bool Notify = false) + private static string CreateSafeKey(string menu, string name) + { + string safeKey = $"{menu}_{name}"; + safeKey = Regex.Replace(safeKey, @"[=\n\t\\""'\[\]]", "_"); + + if (string.IsNullOrEmpty(safeKey)) + safeKey = "default_key"; + + return safeKey; + } + + /// + /// Saves all current settings to the BepInEx configuration file + /// + public static void SaveSettings() + { + if (_isLoading) return; + + try + { + bool configChanged = false; + + foreach (var option in AllOptions) + { + string safeKey = CreateSafeKey(option.Menu.ToString(), option.Name); + + if (option.Type == CustomOptionType.Role) + { + var valueEntry = PeasAPI.ConfigFile.Bind("Settings", $"{safeKey}_Value", + option.DefaultValue?.ToString(), + $"Custom option: {option.Name} (Value)"); + + var value2Entry = PeasAPI.ConfigFile.Bind("Settings", $"{safeKey}_Value2", + option.DefaultValue2?.ToString(), + $"Custom option: {option.Name} (Value2)"); + + if (valueEntry.Value != option.ValueObject?.ToString()) + { + valueEntry.Value = option.ValueObject?.ToString(); + configChanged = true; + } + + if (value2Entry.Value != option.ValueObject2?.ToString()) + { + value2Entry.Value = option.ValueObject2?.ToString(); + configChanged = true; + } + } + else + { + var valueEntry = PeasAPI.ConfigFile.Bind("Settings", safeKey, + option.DefaultValue?.ToString(), + $"Custom option: {option.Name}"); + + if (valueEntry.Value != option.ValueObject?.ToString()) + { + valueEntry.Value = option.ValueObject?.ToString(); + configChanged = true; + } + } + } + + if (configChanged) + { + PeasAPI.ConfigFile.Save(); + PeasAPI.Logger.LogInfo("Settings have been saved to the BepInEx configuration file"); + } + } + catch (Exception ex) + { + PeasAPI.Logger.LogError($"Error occurred while saving settings: {ex.Message}"); + } + } + + /// + /// Loads settings from the BepInEx configuration file + /// + public static void LoadSettings() + { + try + { + _isLoading = true; + + foreach (var option in AllOptions) + { + string safeKey = CreateSafeKey(option.Menu.ToString(), option.Name); + + try + { + if (option.Type == CustomOptionType.Role) + { + // Role options have two values + var valueEntry = PeasAPI.ConfigFile.Bind("Settings", $"{safeKey}_Value", + option.DefaultValue?.ToString(), + $"Custom option: {option.Name} (Value)"); + + var value2Entry = PeasAPI.ConfigFile.Bind("Settings", $"{safeKey}_Value2", + option.DefaultValue2?.ToString(), + $"Custom option: {option.Name} (Value2)"); + + object value = ConvertValue(valueEntry.Value, option.Type); + object value2 = ConvertValue(value2Entry.Value, option.Type); + + option.Set(value, value2, SendRpc: false, Notify: false); + } + else + { + // Other options have only one value + var valueEntry = PeasAPI.ConfigFile.Bind("Settings", safeKey, + option.DefaultValue?.ToString(), + $"Custom option: {option.Name}"); + + object value = ConvertValue(valueEntry.Value, option.Type); + option.Set(value, null, SendRpc: false, Notify: false); + } + } + catch (Exception ex) + { + PeasAPI.Logger.LogWarning($"Error occurred while loading option {option.Name}: {ex.Message}"); + } + } + + PeasAPI.Logger.LogInfo("Settings have been loaded from the BepInEx configuration file"); + } + catch (Exception ex) + { + PeasAPI.Logger.LogError($"Error occurred while loading settings: {ex.Message}"); + } + finally + { + _isLoading = false; + } + } + + private static object ConvertValue(string value, CustomOptionType type) { - //PeasAPI.Logger.LogInfo($"{Name} set to {value}"); + if (string.IsNullOrEmpty(value)) return null; + switch (type) + { + case CustomOptionType.Toggle: + return bool.Parse(value); + case CustomOptionType.Number: + return float.Parse(value); + case CustomOptionType.String: + return int.Parse(value); + case CustomOptionType.Role: + return int.Parse(value); + default: + return value; + } + } + + public void Set(object value, object value2 = null, bool SendRpc = true, bool Notify = false) + { ValueObject = value; + ValueObject2 = value2; + + if (!_isLoading && AmongUsClient.Instance?.AmHost == true) + { + SaveSettings(); + } if (Setting != null && AmongUsClient.Instance.AmHost && SendRpc) Coroutines.Start(RpcUpdateSetting.SendRpc(this)); @@ -96,6 +255,17 @@ public void Set(object value, bool SendRpc = true, bool Notify = false) number.Value = number.oldValue = newValue; number.ValueText.text = ToString(); } + else if (Setting is RoleOptionSetting roleOption) + { + var newValue = (int)ValueObject; + var newValue2 = (int)ValueObject2; + + roleOption.roleChance = newValue; + roleOption.chanceText.text = ToString(); + + roleOption.roleMaxCount = newValue2; + roleOption.countText.text = ToString2(); + } else if (Setting is StringOption str) { var newValue = (int)ValueObject; @@ -109,7 +279,14 @@ public void Set(object value, bool SendRpc = true, bool Notify = false) } if (HudManager.InstanceExists && Type != CustomOptionType.Header && Notify) - HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, ToString(), - HudManager.Instance.Notifier.lastMessageKey != (int)StringName); + if (IsRoleOption) + { + HudManager.Instance.Notifier.AddRoleSettingsChangeMessage(StringName, (int)ValueObject2, (int)ValueObject, RoleTeamTypes.Crewmate); + } + else + { + HudManager.Instance.Notifier.AddSettingsChangeMessage(StringName, ToString(), + HudManager.Instance.Notifier.lastMessageKey != (int)StringName); + } } } \ No newline at end of file diff --git a/PeasAPI/Options/CustomOptionType.cs b/PeasAPI/Options/CustomOptionType.cs index 6a97d1d..91eb86a 100644 --- a/PeasAPI/Options/CustomOptionType.cs +++ b/PeasAPI/Options/CustomOptionType.cs @@ -6,14 +6,7 @@ public enum CustomOptionType Toggle, Number, String, - Button -} - -public enum CustomRoleOptionType -{ - Chance, - Count, - None + Role } public enum MultiMenu diff --git a/PeasAPI/Options/CustomRoleOption.cs b/PeasAPI/Options/CustomRoleOption.cs index b3c7a70..14320e6 100644 --- a/PeasAPI/Options/CustomRoleOption.cs +++ b/PeasAPI/Options/CustomRoleOption.cs @@ -7,13 +7,11 @@ namespace PeasAPI.Options; public class CustomRoleOption : CustomOption { - internal CustomNumberOption chanceOption; - internal CustomNumberOption countOption; internal CustomOption[] AdvancedOptions; public CustomRoleOption(BaseRole baseRole, string prefix, CustomOption[] advancedOptions, MultiMenu menu = MultiMenu.NULL) : base(num++, menu == MultiMenu.NULL ? GetMultiMenu(baseRole) : menu, - Utility.ColorString(baseRole.Color, baseRole.Name), CustomOptionType.Header, 0, isRoleOption: true) + Utility.ColorString(baseRole.Color, baseRole.Name), CustomOptionType.Role, baseRole.Chance, baseRole.Count, baseRole: baseRole, isRoleOption: true) { List removedOptions = new List(); if (advancedOptions != null) @@ -28,13 +26,6 @@ public CustomRoleOption(BaseRole baseRole, string prefix, CustomOption[] advance } } - chanceOption = new CustomNumberOption(menu == MultiMenu.NULL ? GetMultiMenu(baseRole) : menu, - "Role Chance", 0, 10, 0, 100, PercentFormat, - CustomRoleOptionType.Chance, baseRole); - countOption = new CustomNumberOption(menu == MultiMenu.NULL ? GetMultiMenu(baseRole) : menu, - "Role Count", 1, 1, 1, 15, null, - CustomRoleOptionType.Count, baseRole); - foreach (var option in removedOptions) { CustomOption.AllOptions.Add(option); @@ -69,13 +60,70 @@ private static MultiMenu GetMultiMenu(BaseRole baseRole) } } + public int ChanceValue => (int)ValueObject; + public int CountValue => (int)ValueObject2; + + public void IncreaseChance() + { + int newChance; + if (ChanceValue + 10 > 100 + 0.001f) + newChance = 0; + else + newChance = ChanceValue + 10; + + Set(newChance, CountValue); + } + + public void DecreaseChance() + { + int newChance; + if (ChanceValue - 10 < 0 - 0.001f) + newChance = 100; + else + newChance = ChanceValue - 10; + + Set(newChance, CountValue); + } + + public void IncreaseCount() + { + int newCount; + if (CountValue + 1 > 15 + 0.001f) + newCount = 0; + else + newCount = CountValue + 1; + + Set(ChanceValue, newCount); + } + + public void DecreaseCount() + { + int newCount; + if (CountValue - 1 < 0 - 0.001f) + newCount = 15; + else + newCount = CountValue - 1; + + Set(ChanceValue, newCount); + } + public int GetChance() { - return (int)chanceOption.Value; + return ChanceValue; } public int GetCount() { - return (int)countOption.Value; + return CountValue; + } + + public override void OptionCreated() + { + base.OptionCreated(); + var roleOption = Setting.Cast(); + roleOption.roleChance = BaseRole.Chance = (int)ValueObject; + roleOption.roleMaxCount = (int)ValueObject2; + roleOption.chanceText.text = ToString(); + roleOption.countText.text = ToString2(); } } \ No newline at end of file diff --git a/PeasAPI/Options/Patches.cs b/PeasAPI/Options/Patches.cs index 4335bb9..0ce23cc 100644 --- a/PeasAPI/Options/Patches.cs +++ b/PeasAPI/Options/Patches.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using AmongUs.GameOptions; using HarmonyLib; -using Il2CppSystem.Text; using PeasAPI.CustomRpc; using Reactor.Utilities; using Reactor.Utilities.Extensions; @@ -120,9 +118,16 @@ internal class SettingsUpdate { public static List Buttons = new(); public static List Tabs = new(); + public static bool firstStart = true; public static void Postfix(GameSettingMenu __instance) { + if (firstStart) + { + CustomOption.LoadSettings(); + firstStart = false; + } + LobbyInfoPane.Instance.EditButton.gameObject.SetActive(false); Buttons.ForEach(x => x?.Destroy()); Tabs.ForEach(x => x?.Destroy()); @@ -234,78 +239,114 @@ public static void CreateSettings(GameSettingMenu __instance, int target, string tabOptions.Children.Clear(); var options = CustomOption.AllOptions.Where(x => x.Menu == menu).ToList(); - if (target < 8) + var num = 1.5f; + + if (target > 3) { - var num = 1.5f; + var header = Object.Instantiate(tabOptions.categoryHeaderOrigin, Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); + header.SetHeader(StringNames.ImpostorsCategory, 20); + header.Title.text = DestroyableSingleton.Instance.GetString(StringNames.RoleQuotaLabel); + header.transform.localScale = Vector3.one * 0.65f; + header.transform.localPosition = new Vector3(-0.9f, num, -2f); + num -= 0.65f; + + var roleHeader = Object.Instantiate(tabOptions.RolesMenu.categoryHeaderEditRoleOrigin, Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); + roleHeader.SetHeader(StringNames.ImpostorsCategory, 20); + roleHeader.Title.text = target == 4 ? "Crewmate Roles" : target == 5 ? "Neutral Roles" : "Impostor Roles"; + roleHeader.Background.color = target == 4 ? Palette.CrewmateBlue : target == 5 ? Color.gray : Palette.ImpostorRed; + roleHeader.transform.localPosition = new Vector3(4.75f, num + 0.2f, -2f); + num -= 0.61f; + } - foreach (var option in options) + foreach (var option in options) + { + if (option.Type == CustomOptionType.Header) { - if (option.Type == CustomOptionType.Header) - { - var header = Object.Instantiate(tabOptions.categoryHeaderOrigin, Vector3.zero, - Quaternion.identity, tabOptions.settingsContainer); - header.SetHeader(StringNames.ImpostorsCategory, 20); - header.Title.text = option.GetName(); - header.transform.localScale = Vector3.one * 0.65f; - header.transform.localPosition = new Vector3(-0.9f, num, -2f); - num -= 0.625f; - continue; - } + var header = Object.Instantiate(tabOptions.categoryHeaderOrigin, Vector3.zero, + Quaternion.identity, tabOptions.settingsContainer); + header.SetHeader(StringNames.ImpostorsCategory, 20); + header.Title.text = option.GetName(); + header.transform.localScale = Vector3.one * 0.65f; + header.transform.localPosition = new Vector3(-0.9f, num, -2f); + num -= 0.625f; + continue; + } - if (option.Type == CustomOptionType.Number) - { - OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.numberOptionOrigin, - Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); - optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); - optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); - SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); - for (var i = 0; i < components.Length; i++) - components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); - - var numberOption = optionBehaviour as NumberOption; - option.Setting = numberOption; - - tabOptions.Children.Add(optionBehaviour); - } + if (option.Type == CustomOptionType.Number) + { + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.numberOptionOrigin, + Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); + optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + + var numberOption = optionBehaviour as NumberOption; + option.Setting = numberOption; + + tabOptions.Children.Add(optionBehaviour); + } - else if (option.Type == CustomOptionType.Toggle) - { - OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.checkboxOrigin, Vector3.zero, - Quaternion.identity, tabOptions.settingsContainer); - optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); - optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); - SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); - for (var i = 0; i < components.Length; i++) - components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); - - var toggleOption = optionBehaviour as ToggleOption; - option.Setting = toggleOption; - - tabOptions.Children.Add(optionBehaviour); - } + else if (option.Type == CustomOptionType.Role) + { + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.RolesMenu.roleOptionSettingOrigin, + Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(-0.39f, num + 0.26f, -2f); + optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + + var roleOptionSettingOption = optionBehaviour as RoleOptionSetting; + roleOptionSettingOption.SetRole(GameOptionsManager.Instance.CurrentGameOptions.RoleOptions, option.BaseRole.RoleBehaviour, 20); + roleOptionSettingOption.ChanceMinusBtn.isInteractable = true; + roleOptionSettingOption.ChancePlusBtn.isInteractable = true; + roleOptionSettingOption.CountMinusBtn.isInteractable = true; + roleOptionSettingOption.CountPlusBtn.isInteractable = true; + roleOptionSettingOption.transform.GetChild(3).GetComponent().color = option.BaseRole.Color; + option.Setting = roleOptionSettingOption; + + tabOptions.Children.Add(optionBehaviour); + } - else if (option.Type == CustomOptionType.String) - { - var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; + else if (option.Type == CustomOptionType.Toggle) + { + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.checkboxOrigin, Vector3.zero, + Quaternion.identity, tabOptions.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); + optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + + var toggleOption = optionBehaviour as ToggleOption; + option.Setting = toggleOption; + + tabOptions.Children.Add(optionBehaviour); + } - OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.stringOptionOrigin, - Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); - optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); - optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); - SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); - for (var i = 0; i < components.Length; i++) - components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + else if (option.Type == CustomOptionType.String) + { + var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; - var stringOption = optionBehaviour as StringOption; - option.Setting = stringOption; + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.stringOptionOrigin, + Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); + optionBehaviour.transform.localPosition = new Vector3(0.95f, num, -2f); + optionBehaviour.SetClickMask(tabOptions.ButtonClickMask); + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); - tabOptions.Children.Add(optionBehaviour); - } + var stringOption = optionBehaviour as StringOption; + option.Setting = stringOption; - num -= 0.45f; - tabOptions.scrollBar.SetYBoundsMax(-num - 1.65f); - option.OptionCreated(); + tabOptions.Children.Add(optionBehaviour); } + + num -= 0.45f; + tabOptions.scrollBar.SetYBoundsMax(-num - 1.65f); + option.OptionCreated(); } for (var i = 0; i < tabOptions.Children.Count; i++) @@ -317,84 +358,6 @@ public static void CreateSettings(GameSettingMenu __instance, int target, string Tabs.Add(tab); tab.SetActive(false); } - - public static void ImportSlot(string preset) - { - System.Console.WriteLine(preset); - - string text; - - try - { - var path = Path.Combine(Application.persistentDataPath, $"{preset}.txt"); - text = File.ReadAllText(path); - } - catch - { - return; - } - - var splitText = text.Split("\n").ToList(); - - while (splitText.Count > 0) - { - var name = splitText[0].Trim(); - splitText.RemoveAt(0); - var option = - CustomOption.AllOptions.FirstOrDefault(o => o.GetName().Equals(name, StringComparison.Ordinal)); - if (option == null) - { - try - { - splitText.RemoveAt(0); - } - catch - { - } - - continue; - } - - var value = splitText[0]; - splitText.RemoveAt(0); - switch (option.Type) - { - case CustomOptionType.Number: - option.Set(float.Parse(value), false); - break; - case CustomOptionType.Toggle: - option.Set(bool.Parse(value), false); - break; - case CustomOptionType.String: - option.Set(int.Parse(value), false); - break; - } - } - - Coroutines.Start(RpcUpdateSetting.SendRpc()); - } - - public static void ExportSlot(string preset) - { - System.Console.WriteLine($"Exporting settings to {preset}"); - - var builder = new StringBuilder(); - foreach (var option in CustomOption.AllOptions) - { - if (option.Type is CustomOptionType.Button or CustomOptionType.Header) continue; - builder.AppendLine(option.GetName()); - builder.AppendLine($"{option.ValueObject}"); - } - - try - { - var path = Path.Combine(Application.persistentDataPath, $"{preset}.txt"); - File.WriteAllText(path, builder.ToString()); - } - catch - { - } - } } [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.SetTab))] @@ -457,8 +420,8 @@ public static void Postfix(LobbyViewSettingsPane __instance) GameObject.Find("RolesTabs")?.Destroy(); var overview = GameObject.Find("OverviewTab"); - overview.transform.localScale += new Vector3(-0.35f, 0f, 0f); - overview.transform.localPosition += new Vector3(-1f, 0f, 0f); + overview.transform.localScale = new Vector3(0.73f, 1f, 1f); + overview.transform.localPosition += new Vector3(-0.8f, 0f, 0f); overview.transform.GetChild(0).GetChild(0).transform.localScale += new Vector3(0.35f, 0f, 0f); overview.transform.GetChild(0).GetChild(0).transform.localPosition += new Vector3(-1f, 0f, 0f); @@ -476,7 +439,7 @@ public static void CreateButton(LobbyViewSettingsPane __instance, int target, st { tab = GameObject.Instantiate(overview, overview.transform.parent); tab.transform.localPosition += new Vector3(2.5f, 0f, 0f) * target; - tab.transform.GetChild(0).GetChild(0).transform.localPosition += new Vector3(-0.5f, 0f, 0f); + tab.transform.GetChild(0).GetChild(0).transform.localPosition += new Vector3(-0.5f, 0f, 0f); tab.name = name; __instance.StartCoroutine(Effects.Lerp(1f, new Action(p => @@ -497,7 +460,7 @@ public static void AddSettings(LobbyViewSettingsPane __instance, MultiMenu menu) { var options = CustomOption.AllOptions.Where(x => x.Menu == menu).ToList(); - var num = 1.5f; + var num = 1.3f; var headingCount = 0; var settingsThisHeader = 0; var settingRowCount = 0; @@ -527,26 +490,41 @@ public static void AddSettings(LobbyViewSettingsPane __instance, MultiMenu menu) else { var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; - if (option.Name.StartsWith("Slot ")) continue; - var panel = Object.Instantiate(__instance.infoPanelOrigin); - panel.transform.SetParent(__instance.settingsContainer); - panel.transform.localScale = Vector3.one; - if (settingsThisHeader % 2 != 0) + if (option.IsRoleOption) { - panel.transform.localPosition = new Vector3(-3f, num, -2f); - num -= 0.85f; + if (settingsThisHeader % 2 != 0) num -= 0.85f; + var panel = Object.Instantiate(__instance.infoPanelRoleOrigin); + panel.transform.SetParent(__instance.settingsContainer); + panel.transform.localScale = Vector3.one; + panel.transform.localPosition = new Vector3(-6.76f, num, -2f); + panel.SetInfo(option.BaseRole.Name, (int)option.ValueObject2, (int)option.ValueObject, 61, option.BaseRole.Color, RoleManager.Instance.AllRoles[8].RoleIconSolid, option.BaseRole.Team == Roles.Team.Crewmate); + __instance.settingsInfo.Add(panel.gameObject); + num -= 0.75f; + headingCount += 1; + settingsThisHeader = 0; } else { - settingRowCount += 1; - panel.transform.localPosition = new Vector3(-9f, num, -2f); - } + var panel = Object.Instantiate(__instance.infoPanelOrigin); + panel.transform.SetParent(__instance.settingsContainer); + panel.transform.localScale = Vector3.one; + if (settingsThisHeader % 2 != 0) + { + panel.transform.localPosition = new Vector3(-3f, num, -2f); + num -= 0.85f; + } + else + { + settingRowCount += 1; + panel.transform.localPosition = new Vector3(-8.9f, num, -2f); + } - settingsThisHeader += 1; - panel.SetInfo(StringNames.ImpostorsCategory, option.ToString(), 61); - panel.titleText.text = option.GetName(); - __instance.settingsInfo.Add(panel.gameObject); + settingsThisHeader += 1; + panel.SetInfo(StringNames.ImpostorsCategory, option.ToString(), 61); + panel.titleText.text = option.GetName(); + __instance.settingsInfo.Add(panel.gameObject); + } } float actual_spacing = (headingCount * 1.05f + settingRowCount * 0.85f) / (headingCount + settingRowCount) * 1.01f; @@ -589,6 +567,78 @@ public static bool Prefix(ToggleOption __instance) } } + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseChance))] + private class RoleOptionSettingPatchIncreaseChance + { + public static bool Prefix(RoleOptionSetting __instance) + { + var option = + CustomOption.AllOptions.FirstOrDefault(option => + option.Setting == __instance); // Works but may need to change to gameObject.name check + if (option is CustomRoleOption roleOption) + { + roleOption.IncreaseChance(); + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.DecreaseChance))] + private class RoleOptionSettingPatchDecreaseChance + { + public static bool Prefix(RoleOptionSetting __instance) + { + var option = + CustomOption.AllOptions.FirstOrDefault(option => + option.Setting == __instance); // Works but may need to change to gameObject.name check + if (option is CustomRoleOption roleOption) + { + roleOption.DecreaseChance(); + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.IncreaseCount))] + private class RoleOptionSettingPatchIncreaseCount + { + public static bool Prefix(RoleOptionSetting __instance) + { + var option = + CustomOption.AllOptions.FirstOrDefault(option => + option.Setting == __instance); // Works but may need to change to gameObject.name check + if (option is CustomRoleOption roleOption) + { + roleOption.IncreaseCount(); + return false; + } + + return true; + } + } + + [HarmonyPatch(typeof(RoleOptionSetting), nameof(RoleOptionSetting.DecreaseCount))] + private class RoleOptionSettingPatchDecreaseCount + { + public static bool Prefix(RoleOptionSetting __instance) + { + var option = + CustomOption.AllOptions.FirstOrDefault(option => + option.Setting == __instance); // Works but may need to change to gameObject.name check + if (option is CustomRoleOption roleOption) + { + roleOption.DecreaseCount(); + return false; + } + + return true; + } + } + [HarmonyPatch(typeof(NumberOption), nameof(NumberOption.Initialize))] private class NumberOptionInitialise { @@ -677,4 +727,33 @@ public static bool Prefix(StringOption __instance) return true; } } + + [HarmonyPatch(typeof(NotificationPopper), nameof(NotificationPopper.AddRoleSettingsChangeMessage))] + public static class NotificationPopperPatch + { + [HarmonyPrefix] + public static bool RoleChangeMsgPatch( + NotificationPopper __instance, + [HarmonyArgument(0)] StringNames key, + [HarmonyArgument(1)] int roleCount, + [HarmonyArgument(2)] int roleChance, + [HarmonyArgument(3)] RoleTeamTypes teamType, + [HarmonyArgument(4)] bool playSound) + { + var item = TranslationController.Instance.GetString( + StringNames.LobbyChangeSettingNotificationRole, + string.Concat( + "", + Palette.CrewmateSettingChangeText.ToTextColor(), + TranslationController.Instance.GetString(key), + "" + ), + "" + roleCount + "", + "" + roleChance + "%" + ); + + __instance.SettingsChangeMessageLogic(key, item, playSound); + return false; + } + } } \ No newline at end of file diff --git a/PeasAPI/PeasAPI-Icon.png b/PeasAPI/PeasAPI-Icon.png index 9f98727e9668b962aac99da9a658afcafe9f7e6f..8ba40c9e3232261a9e3f97e083ce58e75e44e92a 100644 GIT binary patch literal 20060 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 z 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 literal 42709 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 KMNUMnLSTY(A! net6.0 - 1.9.0 + 1.9.1 release API for making Among Us mods - Peasplayer + FangkuaiYa latest - + embedded + PeasAPI-R PeasAPI-Icon.png + README.md git https://github.com/fangkuaiclub/PeasAPI-R AGPL-3.0-only @@ -19,6 +21,8 @@ + + diff --git a/PeasAPI/Roles/BaseRole.cs b/PeasAPI/Roles/BaseRole.cs index c49bbe3..148896e 100644 --- a/PeasAPI/Roles/BaseRole.cs +++ b/PeasAPI/Roles/BaseRole.cs @@ -240,7 +240,17 @@ public virtual void OnRevive(PlayerControl player) public virtual void OnTaskComplete(PlayerControl player, PlayerTask task) { } - + + public int GetCount() + { + return Option?.GetCount() ?? Count; + } + + public int GetChance() + { + return Option?.GetChance() ?? Chance; + } + public BaseRole(BasePlugin plugin) { Id = RoleManager.GetRoleId(); diff --git a/PeasAPI/Utility.cs b/PeasAPI/Utility.cs index c8cfb23..470d95d 100644 --- a/PeasAPI/Utility.cs +++ b/PeasAPI/Utility.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -41,6 +43,52 @@ public static List GetAllPlayers() return GameData.Instance.AllPlayers.ToArray().ToList().ConvertAll(p => p.Object); } + public static void OverrideOnClickListeners(this PassiveButton passive, Action action, bool enabled = true) + { + passive.OnClick?.RemoveAllListeners(); + passive.OnClick = new(); + passive.OnClick.AddListener(action); + passive.enabled = enabled; + } + + public static void OverrideOnMouseOverListeners(this PassiveButton passive, Action action, bool enabled = true) + { + passive.OnMouseOver?.RemoveAllListeners(); + passive.OnMouseOver = new(); + passive.OnMouseOver.AddListener(action); + passive.enabled = enabled; + } + + public static void OverrideOnMouseOutListeners(this PassiveButton passive, Action action, bool enabled = true) + { + passive.OnMouseOut?.RemoveAllListeners(); + passive.OnMouseOut = new(); + passive.OnMouseOut.AddListener(action); + passive.enabled = enabled; + } + public static void ForEach(this IEnumerable source, Action action) + { + foreach (var item in source) + action(item); + } + + public static IEnumerator PerformTimedAction(float duration, Action action) + { + for (var t = 0f; t < duration; t += Time.deltaTime) + { + action(t / duration); + yield return EndFrame(); + } + + action(1f); + yield break; + } + + public static IEnumerator EndFrame() + { + yield return new WaitForEndOfFrame(); + } + public class StringColor { public const string Reset = ""; diff --git a/README.md b/README.md index 855dbab..9296130 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,37 @@ # PeasAPI -API for making Among Us mods +PeasAPI is an API for developing *Among Us* mods, designed to provide convenient functional support for mod developers and simplify the mod development process. -*This mod is not affiliated with Among Us or Innersloth LLC, and the content contained therein is not endorsed or otherwise sponsored by Innersloth LLC. Portions of the materials contained herein are property of Innersloth LLC. © Innersloth LLC.* +## Features + +1. **Role Management**: Offers rich role-related functionalities, supporting the creation, assignment, and management of custom roles. It enables easy implementation of team judgment between roles, role attribute settings, and more. + +2. **Game Mode Expansion**: Supports custom game modes, allowing switching and configuration of different game modes within the game. + +3. **Network Communication**: Integrates network RPC communication capabilities to facilitate synchronization of game states, role information, and other data among players. + +4. **Update Management**: Equipped with automatic update checking. It can detect updates for related mods, prompt users, and support fetching updates from GitHub repositories. + +5. **Configuration Options**: Allows developers to configure the plugin through configuration files, including log switches, custom server configurations, etc. + +6. **Extended Tools**: Provides various practical extension methods such as player object acquisition, color processing, vector operations, etc., to simplify the development process. + +7. **Watermark Management**: Supports adding custom watermark information to the game interface, with the ability to adjust the watermark position and content according to the game state. + +## Installation Instructions + +1. Ensure the BepInEx framework is installed. +2. Place `PeasAPI.dll` into the `BepInEx\plugins` folder under the *Among Us* game directory. +3. Launch the game to load the plugin. + +## Usage Method + +Search for PeasAPI-R in the Nuget package manager of your programming software to add the dependency to your project for use. + +## License + +This project is open-source under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3. For details, please refer to the [LICENSE](LICENSE) file. + +## Disclaimer + +This mod is not affiliated with *Among Us* or Innersloth LLC, and the content contained herein is not endorsed or sponsored by Innersloth LLC. Portions of the materials are the property of Innersloth LLC. © Innersloth LLC. \ No newline at end of file From ee57a42e4ce0693f289a83743430786a0df41dc6 Mon Sep 17 00:00:00 2001 From: FangkuaiYa <2683748223@qq.com> Date: Tue, 26 Aug 2025 14:02:54 +0800 Subject: [PATCH 4/4] 1.9.2 --- PeasAPI/Managers/CustomNamePlateManager.cs | 246 ++++++++++++++++++ PeasAPI/Managers/UpdateManager.cs | 7 + PeasAPI/Managers/UpdateTools/GitHubUpdater.cs | 6 +- PeasAPI/Options/CustomRoleOption.cs | 12 +- PeasAPI/Options/Patches.cs | 226 +++++++++++----- PeasAPI/PeasAPI.cs | 4 +- PeasAPI/PeasAPI.csproj | 14 +- PeasAPI/Resources/Cog.png | Bin 0 -> 3214 bytes PeasAPI/{ => Resources}/Placeholder.png | Bin PeasAPI/Roles/BaseRole.cs | 10 +- PeasAPI/Utility.cs | 19 ++ 11 files changed, 461 insertions(+), 83 deletions(-) create mode 100644 PeasAPI/Managers/CustomNamePlateManager.cs create mode 100644 PeasAPI/Resources/Cog.png rename PeasAPI/{ => Resources}/Placeholder.png (100%) diff --git a/PeasAPI/Managers/CustomNamePlateManager.cs b/PeasAPI/Managers/CustomNamePlateManager.cs new file mode 100644 index 0000000..70eba0c --- /dev/null +++ b/PeasAPI/Managers/CustomNamePlateManager.cs @@ -0,0 +1,246 @@ +using System.Collections.Generic; +using HarmonyLib; +using UnityEngine; +using System.Linq; +using Innersloth.Assets; +using System; +using UnityEngine.AddressableAssets; +using AmongUs.Data; +using Reactor.Utilities.Extensions; +using Reactor.Utilities; +using TMPro; + +namespace PeasAPI.Managers +{ + public static class CustomNamePlateManager + { + public struct CustomNamePlateData + { + public string Name; + public string Author; + public Sprite Image; + public string Group; + } + + public static bool _customNameplatesLoaded = false; + static readonly List namePlateData = new(); + private static readonly List customPlateData = new(); + public static readonly Dictionary CustomNameplateViewDatas = []; + public static readonly Dictionary> RegisteredNamePlates = new(); + + public static void RegisterNewNamePlate(string name, Sprite image, string author = "Unknown", string group = "Custom") + { + if (!RegisteredNamePlates.ContainsKey(group)) + { + RegisteredNamePlates[group] = new List(); + } + + RegisteredNamePlates[group].Add(new CustomNamePlateData + { + Name = name, + Author = author, + Image = image, + Group = group + }); + } + + [HarmonyPatch(typeof(HatManager), nameof(HatManager.GetNamePlateById))] + class UnlockedNamePlatesPatch + { + public static void Postfix(HatManager __instance) + { + if (_customNameplatesLoaded) return; + _customNameplatesLoaded = true; + var AllPlates = __instance.allNamePlates.ToList(); + + foreach (var group in RegisteredNamePlates) + { + foreach (var data in group.Value) + { + NamePlateViewData nvd = new NamePlateViewData(); + nvd.Image = data.Image; + + var nameplate = new CustomNamePlates(nvd); + nameplate.name = $"{data.Name} (by {data.Author})"; + nameplate.ProductId = "lmj_" + nameplate.name.Replace(' ', '_'); + nameplate.BundleId = "lmj_" + nameplate.name.Replace(' ', '_'); + nameplate.displayOrder = 99; + nameplate.ChipOffset = new Vector2(0f, 0.2f); + nameplate.Free = true; + namePlateData.Add(nameplate); + customPlateData.Add(nameplate); + var assetRef = new AssetReference(nvd.Pointer); + nameplate.ViewDataRef = assetRef; + nameplate.CreateAddressableAsset(); + CustomNameplateViewDatas.TryAdd(nameplate.ProductId, nvd); + } + } + + AllPlates.AddRange(namePlateData); + __instance.allNamePlates = AllPlates.ToArray(); + } + } + + [HarmonyPatch(typeof(NameplatesTab), nameof(NameplatesTab.OnEnable))] + public static class NameplatesTabOnEnablePatch + { + private static TMP_Text Template; + + private static float CreateNameplatePackage(List nameplates, string packageName, float YStart, NameplatesTab __instance) + { + + var offset = YStart; + + if (Template) + { + var title = UnityEngine.Object.Instantiate(Template, __instance.scroller.Inner); + var material = title.GetComponent().material; + material.SetFloat("_StencilComp", 4f); + material.SetFloat("_Stencil", 1f); + title.transform.localPosition = new(2.25f, YStart, -1f); + title.transform.localScale = Vector3.one * 1.5f; + title.fontSize *= 0.5f; + title.enableAutoSizing = false; + Coroutines.Start(Utility.PerformTimedAction(0.1f, _ => title.SetText(packageName, true))); + offset -= 0.8f * __instance.YOffset; + } + + for (var i = 0; i < nameplates.Count; i++) + { + var nameplate = nameplates[i]; + var xpos = __instance.XRange.Lerp(i % __instance.NumPerRow / (__instance.NumPerRow - 1f)); + var ypos = offset - (i / __instance.NumPerRow * __instance.YOffset); + var colorChip = UnityEngine.Object.Instantiate(__instance.ColorTabPrefab, __instance.scroller.Inner); + + if (ActiveInputManager.currentControlType == ActiveInputManager.InputType.Keyboard) + { + colorChip.Button.OverrideOnMouseOverListeners(() => __instance.SelectNameplate(nameplate)); + colorChip.Button.OverrideOnMouseOutListeners(() => __instance.SelectNameplate(HatManager.Instance.GetNamePlateById(DataManager.Player.Customization.NamePlate))); + colorChip.Button.OverrideOnClickListeners(__instance.ClickEquip); + } + else + colorChip.Button.OverrideOnClickListeners(() => __instance.SelectNameplate(nameplate)); + + colorChip.Button.ClickMask = __instance.scroller.Hitbox; + colorChip.transform.localPosition = new(xpos, ypos, -1f); + colorChip.ProductId = nameplate.ProductId; + colorChip.Tag = nameplate; + colorChip.SelectionHighlight.gameObject.SetActive(false); + + if (CustomNameplateViewDatas.TryGetValue(colorChip.ProductId, out var viewData)) + colorChip.gameObject.GetComponent().image.sprite = viewData.Image; + else + DefaultNameplateCoro(__instance, colorChip.gameObject.GetComponent()); + + __instance.ColorChips.Add(colorChip); + } + + return offset - ((nameplates.Count - 1) / __instance.NumPerRow * __instance.YOffset) - 1.5f; + } + + private static void DefaultNameplateCoro(NameplatesTab __instance, NameplateChip chip) => __instance.StartCoroutine(__instance.CoLoadAssetAsync(HatManager.Instance + .GetNamePlateById(chip.ProductId).ViewDataRef, (Action)(viewData => chip.image.sprite = viewData?.Image))); + + public static bool Prefix(NameplatesTab __instance) + { + for (var i = 0; i < __instance.scroller.Inner.childCount; i++) + __instance.scroller.Inner.GetChild(i).gameObject.Destroy(); + + __instance.ColorChips = new(); + var array = HatManager.Instance.GetUnlockedNamePlates(); + var packages = new Dictionary>(); + + foreach (var data in array) + { + + var package = "Innersloth"; + + if (data.ProductId.StartsWith("lmj_")) + { + // 查找这个名牌属于哪个组 + package = "Custom"; + foreach (var group in RegisteredNamePlates) + { + if (customPlateData.Any(v => v.ProductId == data.ProductId && + group.Value.Any(vd => $"{vd.Name} (by {vd.Author})" == v.name))) + { + package = group.Key; + break; + } + } + } + + if (!packages.ContainsKey(package)) + packages[package] = []; + + packages[package].Add(data); + } + + var YOffset = __instance.YStart; + Template = __instance.transform.FindChild("Text").gameObject.GetComponent(); + var keys = packages.Keys.OrderBy(x => x switch + { + "Innersloth" => 999, // 确保官方内容在最后 + _ => Array.IndexOf(RegisteredNamePlates.Keys.ToArray(), x) // 自定义组按注册顺序 + }); + + keys.ForEach(key => YOffset = CreateNameplatePackage(packages[key], key, YOffset, __instance)); + + if (array.Length != 0) + __instance.GetDefaultSelectable().PlayerEquippedForeground.SetActive(true); + + __instance.plateId = DataManager.Player.Customization.NamePlate; + __instance.currentNameplateIsEquipped = true; + __instance.SetScrollerBounds(); + __instance.scroller.ContentYBounds.max = -(YOffset + 3.8f); + return false; + } + } + + static Dictionary cache = new(); + static NamePlateViewData GetByCache(string id) + { + if (!cache.ContainsKey(id)) + { + cache[id] = customPlateData.FirstOrDefault(x => x.ProductId == id)?.nameplateViewData; + } + return cache[id]; + } + + [HarmonyPatch(typeof(CosmeticsCache), nameof(CosmeticsCache.GetNameplate))] + class CosmeticsCacheGetPlatePatch + { + public static bool Prefix(CosmeticsCache __instance, string id, ref NamePlateViewData __result) + { + if (!id.StartsWith("lmj_")) return true; + __result = GetByCache(id); + if (__result == null) + __result = __instance.nameplates["nameplate_NoPlate"].GetAsset(); + return false; + } + } + + [HarmonyPatch(typeof(PlayerVoteArea), nameof(PlayerVoteArea.PreviewNameplate))] + class PreviewNameplatesPatch + { + public static void Postfix(PlayerVoteArea __instance, string plateID) + { + if (!plateID.StartsWith("lmj_")) return; + NamePlateViewData npvd = GetByCache(plateID); + if (npvd != null) + { + __instance.Background.sprite = npvd.Image; + } + } + } + } + + class CustomNamePlates : NamePlateData + { + public NamePlateViewData nameplateViewData; + public CustomNamePlates(NamePlateViewData hvd) + { + nameplateViewData = hvd; + } + } +} \ No newline at end of file diff --git a/PeasAPI/Managers/UpdateManager.cs b/PeasAPI/Managers/UpdateManager.cs index 41561d9..1ef75f4 100644 --- a/PeasAPI/Managers/UpdateManager.cs +++ b/PeasAPI/Managers/UpdateManager.cs @@ -17,6 +17,13 @@ public static class UpdateManager public static bool DoUpdateChecks = true; private static readonly List UpdateListeners = new(); + /// + /// Note: PeasAPI's update detection determines whether there is an update by checking the `tag_name` of the latest release on GitHub and the Assembly version of the mod. The `tag_name` should be written as "v1.0.0" or "1.0.0". + /// + /// + /// + /// + /// public static void RegisterGitHubUpdateListener(string owner, string repoName, UpdateType updateType = UpdateType.Every, FileType type = FileType.Dll) { var callingAssembly = Assembly.GetCallingAssembly(); diff --git a/PeasAPI/Managers/UpdateTools/GitHubUpdater.cs b/PeasAPI/Managers/UpdateTools/GitHubUpdater.cs index 72bfdb6..a375e52 100644 --- a/PeasAPI/Managers/UpdateTools/GitHubUpdater.cs +++ b/PeasAPI/Managers/UpdateTools/GitHubUpdater.cs @@ -52,8 +52,10 @@ private string GetLinkByPriority(IReadOnlyCollection array) var first = array.Cast().FirstOrDefault(x => x?.GetProperty("content_type").GetString() ?.Equals(priority) ?? true) ?? array.FirstOrDefault(); - - return first.GetProperty("browser_download_url").GetString(); + + var dlURLPre = Utility.isChinese() ? "https://ghproxy.fangkuai.fun/" : ""; + + return dlURLPre + first.GetProperty("browser_download_url").GetString(); } } } \ No newline at end of file diff --git a/PeasAPI/Options/CustomRoleOption.cs b/PeasAPI/Options/CustomRoleOption.cs index 14320e6..3f52656 100644 --- a/PeasAPI/Options/CustomRoleOption.cs +++ b/PeasAPI/Options/CustomRoleOption.cs @@ -2,6 +2,14 @@ using System.Collections.Generic; using System.Linq; using PeasAPI.Roles; +using Reactor.Utilities.Extensions; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; +using UnityEngine.UIElements.UIR; +using static UnityEngine.UI.Button; +using Object = UnityEngine.Object; namespace PeasAPI.Options; @@ -11,13 +19,14 @@ public class CustomRoleOption : CustomOption public CustomRoleOption(BaseRole baseRole, string prefix, CustomOption[] advancedOptions, MultiMenu menu = MultiMenu.NULL) : base(num++, menu == MultiMenu.NULL ? GetMultiMenu(baseRole) : menu, - Utility.ColorString(baseRole.Color, baseRole.Name), CustomOptionType.Role, baseRole.Chance, baseRole.Count, baseRole: baseRole, isRoleOption: true) + Utility.ColorString(baseRole.Color, baseRole.Name), CustomOptionType.Role, baseRole.Chance, baseRole.Count, baseRole: baseRole) { List removedOptions = new List(); if (advancedOptions != null) { foreach (var option in advancedOptions) { + option.IsRoleOption = true; if (option != null && CustomOption.AllOptions.Contains(option)) { removedOptions.Add(option); @@ -125,5 +134,6 @@ public override void OptionCreated() roleOption.roleMaxCount = (int)ValueObject2; roleOption.chanceText.text = ToString(); roleOption.countText.text = ToString2(); + roleOption.transform.GetChild(0).GetComponent().alignment = TextAlignmentOptions.Left; } } \ No newline at end of file diff --git a/PeasAPI/Options/Patches.cs b/PeasAPI/Options/Patches.cs index 0ce23cc..881f42d 100644 --- a/PeasAPI/Options/Patches.cs +++ b/PeasAPI/Options/Patches.cs @@ -4,10 +4,13 @@ using AmongUs.GameOptions; using HarmonyLib; using PeasAPI.CustomRpc; +using PeasAPI.Roles; using Reactor.Utilities; using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; +using UnityEngine.Events; +using static UnityEngine.UI.Button; using Object = UnityEngine.Object; namespace PeasAPI.Options; @@ -48,6 +51,12 @@ private class ChangeTab { public static void Postfix(GameSettingMenu __instance, int tabNum, bool previewOnly) { + if (SettingsUpdate.customRolesSettings != null) + { + SettingsUpdate.customRolesSettings.gameObject.Destroy(); + SettingsUpdate.customRolesSettings = null; + } + if (previewOnly) return; foreach (var tab in SettingsUpdate.Tabs) if (tab != null) @@ -56,17 +65,25 @@ public static void Postfix(GameSettingMenu __instance, int tabNum, bool previewO if (tabNum > 2) { tabNum -= 3; - SettingsUpdate.Tabs[tabNum].SetActive(true); + if (tabNum < SettingsUpdate.Tabs.Count && SettingsUpdate.Tabs[tabNum] != null) // Added null check + SettingsUpdate.Tabs[tabNum].SetActive(true); if (tabNum > 4) return; - SettingsUpdate.Buttons[tabNum].SelectButton(true); + if (tabNum < SettingsUpdate.Buttons.Count && SettingsUpdate.Buttons[tabNum] != null) // Added null check + SettingsUpdate.Buttons[tabNum].SelectButton(true); __instance.StartCoroutine(Effects.Lerp(1f, new Action(p => { + __instance.RoleSettingsTab.gameObject.SetActive(false); + foreach (var option in CustomOption.AllOptions) + { + if (option?.Setting == null) continue; // Added null check + if (option.Type == CustomOptionType.Number) { - var number = option.Setting.Cast(); + var number = option.Setting.TryCast(); + if (number?.TitleText == null) continue; // Added null check number.TitleText.text = option.GetName(); if (number.TitleText.text.StartsWith("(); + var tgl = option.Setting.TryCast(); + if (tgl?.TitleText == null) continue; // Added null check tgl.TitleText.text = option.GetName(); if (tgl.TitleText.text.Length > 20) tgl.TitleText.fontSize = 2.25f; @@ -87,11 +104,10 @@ public static void Postfix(GameSettingMenu __instance, int tabNum, bool previewO tgl.TitleText.fontSize = 2f; else tgl.TitleText.fontSize = 2.75f; } - else if (option.Type == CustomOptionType.String) { - var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; - var str = option.Setting.Cast(); + var str = option.Setting.TryCast(); + if (str?.TitleText == null) continue; // Added null check str.TitleText.text = option.GetName(); if (str.TitleText.text.Length > 20) str.TitleText.fontSize = 2.25f; @@ -99,6 +115,7 @@ public static void Postfix(GameSettingMenu __instance, int tabNum, bool previewO str.TitleText.fontSize = 2f; else str.TitleText.fontSize = 2.75f; } + } }))); } } @@ -128,6 +145,7 @@ public static void Postfix(GameSettingMenu __instance) firstStart = false; } + __instance.GameSettingsButton.OnMouseOver.RemoveAllListeners(); LobbyInfoPane.Instance.EditButton.gameObject.SetActive(false); Buttons.ForEach(x => x?.Destroy()); Tabs.ForEach(x => x?.Destroy()); @@ -146,68 +164,14 @@ public static void Postfix(GameSettingMenu __instance) settingsButton.transform.localScale *= 0.9f; CreateSettings(__instance, 3, "ModSettings", "Mod Settings", settingsButton, MultiMenu.Main); - CreateSettings(__instance, 4, "CrewSettings", "Crewmate Settings", settingsButton, - MultiMenu.Crewmate); - CreateSettings(__instance, 5, "NeutralSettings", "Neutral Settings", settingsButton, - MultiMenu.Neutral); - CreateSettings(__instance, 6, "ImpSettings", "Impostor Settings", settingsButton, - MultiMenu.Impostor); + CreateSettings(__instance, 4, "CrewSettings", "Crewmate Settings", settingsButton, MultiMenu.Crewmate); + CreateSettings(__instance, 5, "NeutralSettings", "Neutral Settings", settingsButton, MultiMenu.Neutral); + CreateSettings(__instance, 6, "ImpSettings", "Impostor Settings", settingsButton, MultiMenu.Impostor); } - internal static TextMeshPro SpawnExternalButton(GameSettingMenu __instance, GameOptionsMenu tabOptions, - ref float num, string text, Action onClick) - { - const float scaleX = 7f; - var baseButton = __instance.GameSettingsTab.checkboxOrigin.transform.GetChild(1); - var baseText = __instance.GameSettingsTab.checkboxOrigin.transform.GetChild(0); - - var exportButtonGO = GameObject.Instantiate(baseButton, Vector3.zero, Quaternion.identity, - tabOptions.settingsContainer); - exportButtonGO.name = text; - exportButtonGO.transform.localPosition = new Vector3(1f, num, -2f); - exportButtonGO.GetComponent().offset = Vector2.zero; - exportButtonGO.name = text.Replace(" ", ""); - - var prevColliderSize = exportButtonGO.GetComponent().size; - prevColliderSize.x *= scaleX; - exportButtonGO.GetComponent().size = prevColliderSize; - - exportButtonGO.transform.GetChild(2).gameObject.DestroyImmediate(); - var exportButton = exportButtonGO.GetComponent(); - exportButton.ClickMask = tabOptions.ButtonClickMask; - exportButton.OnClick.RemoveAllListeners(); - exportButton.OnClick.AddListener(onClick); - - var exportButtonTextGO = GameObject.Instantiate(baseText, exportButtonGO); - exportButtonTextGO.transform.localPosition = new Vector3(0, 0, -3f); - exportButtonTextGO.GetComponent().SetSize(prevColliderSize.x, prevColliderSize.y); - var exportButtonText = exportButtonTextGO.GetComponent(); - exportButtonText.alignment = TextAlignmentOptions.Center; - exportButtonText.SetText(text); - - SpriteRenderer[] componentsInChildren = exportButtonGO.GetComponentsInChildren(true); - for (var i = 0; i < componentsInChildren.Length; i++) - { - componentsInChildren[i].material.SetInt(PlayerMaterial.MaskLayer, 20); - componentsInChildren[i].transform.localPosition = new Vector3(0, 0, -1); - var prevSpriteSize = componentsInChildren[i].size; - prevSpriteSize.x *= scaleX; - componentsInChildren[i].size = prevSpriteSize; - } - - TextMeshPro[] componentsInChildren2 = exportButtonGO.GetComponentsInChildren(true); - foreach (var obj in componentsInChildren2) - { - obj.fontMaterial.SetFloat("_StencilComp", 3f); - obj.fontMaterial.SetFloat("_Stencil", 20); - } - - num -= 0.6f; - return exportButtonText; - } + internal static RolesSettingsMenu customRolesSettings = null; - public static void CreateSettings(GameSettingMenu __instance, int target, string name, string text, - GameObject settingsButton, MultiMenu menu) + public static void CreateSettings(GameSettingMenu __instance, int target, string name, string text, GameObject settingsButton, MultiMenu menu) { var panel = GameObject.Find("LeftPanel"); var button = GameObject.Find(name); @@ -251,7 +215,7 @@ public static void CreateSettings(GameSettingMenu __instance, int target, string num -= 0.65f; var roleHeader = Object.Instantiate(tabOptions.RolesMenu.categoryHeaderEditRoleOrigin, Vector3.zero, Quaternion.identity, tabOptions.settingsContainer); - roleHeader.SetHeader(StringNames.ImpostorsCategory, 20); + roleHeader.SetHeader(target == 6 ? StringNames.ImpostorRolesHeader : StringNames.CrewmateRolesHeader, 20); roleHeader.Title.text = target == 4 ? "Crewmate Roles" : target == 5 ? "Neutral Roles" : "Impostor Roles"; roleHeader.Background.color = target == 4 ? Palette.CrewmateBlue : target == 5 ? Color.gray : Palette.ImpostorRed; roleHeader.transform.localPosition = new Vector3(4.75f, num + 0.2f, -2f); @@ -260,6 +224,9 @@ public static void CreateSettings(GameSettingMenu __instance, int target, string foreach (var option in options) { + if (option.IsRoleOption) + continue; + if (option.Type == CustomOptionType.Header) { var header = Object.Instantiate(tabOptions.categoryHeaderOrigin, Vector3.zero, @@ -308,6 +275,35 @@ public static void CreateSettings(GameSettingMenu __instance, int target, string option.Setting = roleOptionSettingOption; tabOptions.Children.Add(optionBehaviour); + + // Role Setting Button + var newButton = Object.Instantiate(roleOptionSettingOption.buttons[0], roleOptionSettingOption.transform); + newButton.name = "ConfigButton"; + newButton.transform.localPosition = new Vector3(0.4473f, -0.3f, -2f); + newButton.transform.FindChild("Text_TMP").gameObject.DestroyImmediate(); + newButton.activeSprites.Destroy(); + + // Read Sprite from Mod Resources + var btnRend = newButton.transform.FindChild("ButtonSprite").GetComponent(); + btnRend.sprite = Utility.CreateSprite("PeasAPI.Resources.Cog.png", 100f); + + var passiveButton = newButton.GetComponent(); + passiveButton.OnClick = new ButtonClickedEvent(); + passiveButton.interactableColor = btnRend.color = Color.white; + passiveButton.interactableHoveredColor = Palette.CrewmateBlue; + passiveButton.OnClick.AddListener((UnityAction)(() => + { + __instance.ToggleLeftSideDarkener(on: true); + __instance.ToggleRightSideDarkener(on: false); + var roleTab = Object.Instantiate(__instance.RoleSettingsTab, __instance.transform); + roleTab.gameObject.SetActive(true); + roleTab.transform.GetChild(1).gameObject.SetActive(false); + roleTab.transform.localPosition = new Vector3(1.4873f, -0.653f, -4f); + customRolesSettings = roleTab; + tabOptions.gameObject.SetActive(false); + // Create custom role tab and change to custom role tab + ChangeTab(roleTab, new ModRoleRulesCategory { Role = option.BaseRole, AllGameSettings = option.BaseRole.AdvancedOptions.Values.ToList() }, tabOptions); + })); } else if (option.Type == CustomOptionType.Toggle) @@ -358,6 +354,98 @@ public static void CreateSettings(GameSettingMenu __instance, int target, string Tabs.Add(tab); tab.SetActive(false); } + + private static void ChangeTab(RolesSettingsMenu __instance, ModRoleRulesCategory cat, GameOptionsMenu tabOptions) + { + CreateAdvancedSettings(__instance, cat, tabOptions); + __instance.roleDescriptionText.text = cat.Role.LongDescription; + __instance.roleTitleText.text = cat.Role.Name; + __instance.roleScreenshot.sprite = cat.Role.Icon; + __instance.roleHeaderSprite.color = cat.Role.Color; + __instance.roleHeaderText.color = Color.white; + __instance.RoleChancesSettings.SetActive(false); + __instance.AdvancedRolesSettings.SetActive(true); + __instance.RefreshChildren(); + ControllerManager.Instance.CurrentUiState.SelectableUiElements = __instance.ControllerSelectable; + } + + private static void CreateAdvancedSettings(RolesSettingsMenu __instance, ModRoleRulesCategory cat, GameOptionsMenu tabOptions) + { + float num = -0.872f; + foreach (CustomOption allGameSetting in cat.AllGameSettings) + { + switch (allGameSetting.Type) + { + case CustomOptionType.Number: + { + OptionBehaviour optionBehaviour = Object.Instantiate(__instance.numberOptionOrigin, Vector3.zero, Quaternion.identity, __instance.AdvancedRolesSettings.transform); + optionBehaviour.transform.localPosition = new Vector3(2.17f, num, -2f); + optionBehaviour.SetClickMask(__instance.ButtonClickMask); + optionBehaviour.LabelBackground.enabled = false; + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + optionBehaviour.AssociatedRole = cat.Role.RoleBehaviour.Role; + + var numberOption = optionBehaviour as NumberOption; + numberOption.MinusBtn.enabled = true; + numberOption.PlusBtn.enabled = true; + allGameSetting.Setting = numberOption; + + tabOptions.Children.Add(optionBehaviour); + break; + } + case CustomOptionType.Toggle: + { + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.checkboxOrigin, Vector3.zero, Quaternion.identity, __instance.AdvancedRolesSettings.transform); + optionBehaviour.transform.localPosition = new Vector3(2.17f, num, -2f); + optionBehaviour.SetClickMask(__instance.ButtonClickMask); + optionBehaviour.LabelBackground.enabled = false; + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + optionBehaviour.AssociatedRole = cat.Role.RoleBehaviour.Role; + + var toggleOption = optionBehaviour as ToggleOption; + allGameSetting.Setting = toggleOption; + + tabOptions.Children.Add(optionBehaviour); + break; + } + case CustomOptionType.String: + { + var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; + + OptionBehaviour optionBehaviour = Object.Instantiate(tabOptions.stringOptionOrigin, Vector3.zero, Quaternion.identity, __instance.AdvancedRolesSettings.transform); + optionBehaviour.transform.localPosition = new Vector3(2.17f, num, -2f); + optionBehaviour.SetClickMask(__instance.ButtonClickMask); + optionBehaviour.LabelBackground.enabled = false; + SpriteRenderer[] components = optionBehaviour.GetComponentsInChildren(true); + for (var i = 0; i < components.Length; i++) + components[i].material.SetInt(PlayerMaterial.MaskLayer, 20); + optionBehaviour.AssociatedRole = cat.Role.RoleBehaviour.Role; + + var stringOption = optionBehaviour as StringOption; + allGameSetting.Setting = stringOption; + + tabOptions.Children.Add(optionBehaviour); + break; + } + } + num -= 0.45f; + __instance.scrollBar.SetYBoundsMax(-num - 1.65f); + allGameSetting.OptionCreated(); + } + __instance.scrollBar.CalculateAndSetYBounds(cat.AllGameSettings.Count + 3, 1f, 6f, 0.45f); + __instance.scrollBar.ScrollToTop(); + } + + public struct ModRoleRulesCategory + { + public BaseRole Role; + + public List AllGameSettings; + } } [HarmonyPatch(typeof(LobbyViewSettingsPane), nameof(LobbyViewSettingsPane.SetTab))] @@ -491,7 +579,7 @@ public static void AddSettings(LobbyViewSettingsPane __instance, MultiMenu menu) { var playerCount = GameOptionsManager.Instance.currentNormalGameOptions.MaxPlayers; - if (option.IsRoleOption) + if (option.Type == CustomOptionType.Role) { if (settingsThisHeader % 2 != 0) num -= 0.85f; var panel = Object.Instantiate(__instance.infoPanelRoleOrigin); diff --git a/PeasAPI/PeasAPI.cs b/PeasAPI/PeasAPI.cs index c3815dd..45a02e9 100644 --- a/PeasAPI/PeasAPI.cs +++ b/PeasAPI/PeasAPI.cs @@ -22,7 +22,7 @@ namespace PeasAPI public class PeasAPI : BasePlugin { public const string Id = "tk.peasplayer.amongus.api"; - public const string VersionString = "1.9.1"; + public const string VersionString = "1.9.2"; public Harmony Harmony { get; } = new Harmony(Id); @@ -74,7 +74,7 @@ public override void Load() ConfigFile.Bind("CustomServer", "Port", (ushort)22023).Value); } - UpdateManager.RegisterGitHubUpdateListener("Peasplayer", "PeasAPI"); + UpdateManager.RegisterGitHubUpdateListener("fangkuaiclub", "PeasAPI-R"); RegisterCustomRoleAttribute.Load(); RegisterCustomGameModeAttribute.Load(); diff --git a/PeasAPI/PeasAPI.csproj b/PeasAPI/PeasAPI.csproj index 594359c..5c1db95 100644 --- a/PeasAPI/PeasAPI.csproj +++ b/PeasAPI/PeasAPI.csproj @@ -1,7 +1,7 @@  net6.0 - 1.9.1 + 1.9.2 release API for making Among Us mods FangkuaiYa @@ -23,11 +23,11 @@ - - - - - + + + + + @@ -35,7 +35,7 @@ - + diff --git a/PeasAPI/Resources/Cog.png b/PeasAPI/Resources/Cog.png new file mode 100644 index 0000000000000000000000000000000000000000..8551db45bfe75cc2ec20a45b8da15bd10f617dc0 GIT binary patch literal 3214 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 AtpET3 literal 0 HcmV?d00001 diff --git a/PeasAPI/Placeholder.png b/PeasAPI/Resources/Placeholder.png similarity index 100% rename from PeasAPI/Placeholder.png rename to PeasAPI/Resources/Placeholder.png diff --git a/PeasAPI/Roles/BaseRole.cs b/PeasAPI/Roles/BaseRole.cs index 148896e..08813d8 100644 --- a/PeasAPI/Roles/BaseRole.cs +++ b/PeasAPI/Roles/BaseRole.cs @@ -25,10 +25,16 @@ public abstract class BaseRole /// The description of the Role. Will displayed at the intro /// public abstract string Description { get; } - + + /// + /// More detailed role description, show in role settings + /// public abstract string LongDescription { get; } - public virtual Sprite Icon { get; } = Utility.CreateSprite("PeasAPI.Placeholder.png"); + /// + /// Used to display images in role settings + /// + public virtual Sprite Icon { get; } = Utility.CreateSprite("PeasAPI.Resources.Placeholder.png"); /// /// The description of the Role at the task list diff --git a/PeasAPI/Utility.cs b/PeasAPI/Utility.cs index 470d95d..34803bc 100644 --- a/PeasAPI/Utility.cs +++ b/PeasAPI/Utility.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -89,6 +90,24 @@ public static IEnumerator EndFrame() yield return new WaitForEndOfFrame(); } + /// + /// Used to detect whether the region where the player is located is China. + /// + /// + public static bool isChinese() + { + try + { + var name = CultureInfo.CurrentUICulture.Name; + if (name.StartsWith("zh")) return true; + return false; + } + catch + { + return false; + } + } + public class StringColor { public const string Reset = "";