diff --git a/.gitignore b/.gitignore index 39c9242..1911d8b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ obj/ riderModule.iml /_ReSharper.Caches/ .idea/ +*.lock.json +.vs/ +*.user +.qodo +*.DS_Store + +.fake \ No newline at end of file diff --git a/AchievementsAPI/API/AchievementStorage.cs b/AchievementsAPI/API/AchievementStorage.cs index d85d756..79683b1 100644 --- a/AchievementsAPI/API/AchievementStorage.cs +++ b/AchievementsAPI/API/AchievementStorage.cs @@ -34,8 +34,38 @@ public static void AchievementStorageUpdate(BaseAchievement achievement, bool un public static void AchievementStorageUpdate(CountAchievement achievement, int progress, bool unlocked) { var data = GetData(achievement); + var newProgress = Math.Clamp(progress, 0, achievement.RequiredValue); data.Unlocked = unlocked; - if (achievement.ProgressPersists) data.Progress = progress; + if (achievement.ProgressPersists is AchPersistence.ThroughoutSessions || data.Unlocked) data.Progress = newProgress; + + Save(); + } + public static void AchievementDataReset(BaseAchievement achievement, bool? unlocked = false) + { + var data = GetData(achievement); + if (unlocked != null) + { + achievement.Unlocked = unlocked.Value; + data.Unlocked = unlocked.Value; + } + + Save(); + } + + public static void AchievementDataReset(CountAchievement achievement, int progress = 0, bool? unlocked = null) + { + var data = GetData(achievement); + data.Progress = Math.Clamp(progress, 0, achievement.RequiredValue); + if (unlocked != null) + { + achievement.Unlocked = unlocked.Value; + data.Unlocked = unlocked.Value; + } + else + { + data.Unlocked = data.Progress == achievement.RequiredValue; + achievement.Unlocked = data.Progress == achievement.RequiredValue; + } Save(); } @@ -74,7 +104,7 @@ public static AchievementData GetData(CountAchievement achievement) public static void AchievementStorageGet(AchievementsTab tab) { foreach (var propInfo in tab.GetType().GetProperties().Where(x => - x.PropertyType == typeof(BaseAchievement))) + x.PropertyType.IsAssignableTo(typeof(BaseAchievement)))) { var achievement = propInfo.GetValue(tab) as BaseAchievement; if (achievement == null) continue; @@ -85,7 +115,7 @@ public static void AchievementStorageGet(AchievementsTab tab) } foreach (var propInfo in tab.GetType().GetProperties().Where(x => - x.PropertyType == typeof(CountAchievement))) + x.PropertyType.IsAssignableTo(typeof(CountAchievement)))) { var achievement = propInfo.GetValue(tab) as CountAchievement; if (achievement == null) continue; diff --git a/AchievementsAPI/API/AchievementToast.cs b/AchievementsAPI/API/AchievementToast.cs index f884ec2..21d3ee9 100644 --- a/AchievementsAPI/API/AchievementToast.cs +++ b/AchievementsAPI/API/AchievementToast.cs @@ -1,10 +1,9 @@ using UnityEngine; using System.Collections; -using Il2CppInterop.Runtime.InteropTypes.Fields; -using Reactor.Utilities; -using Reactor.Utilities.Extensions; using Rewired.Utils; using UnityEngine.UI; +using VentLib.Utilities; +using VentLib.Utilities.Extensions; namespace AchievementsAPI.API; public class AchievementToast @@ -13,12 +12,12 @@ public class AchievementToast public static void ShowAndDeleteToast(BaseAchievement achievement) { - Coroutines.Start(CoShowAndDeleteToast(achievement)); + Async.Execute(CoShowAndDeleteToast(achievement)); } public static void ShowAndDeleteToast(CountAchievement achievement, bool unlocked = false) { - Coroutines.Start(CoShowAndDeleteToast(achievement, unlocked)); + Async.Execute(CoShowAndDeleteToast(achievement, unlocked)); } private static Transform GetOrCreateToast() @@ -42,11 +41,17 @@ private static Transform GetOrCreateToast() } } - private static void PopulateToast(Transform toast, Sprite icon, System.Reflection.Assembly assembly, string title, string subtitle) + private static void PopulateToast(Transform toast, Sprite icon, System.Reflection.Assembly assembly, string title, string subtitle, Vector3 titleOffset, Vector3 subtitleOffset, Vector3 iconOffset) { - toast.FindChild("AchievementIcon").gameObject.GetComponent().sprite = icon; - toast.FindChild("AchievementName").gameObject.GetComponent().text = title; - toast.FindChild("AchievementObtainedText").gameObject.GetComponent().text = subtitle; + var icoObj = toast.FindChild("AchievementIcon").gameObject.GetComponent(); + icoObj.sprite = icon; + icoObj.transform.localPosition += iconOffset; + var titleObj = toast.FindChild("AchievementName").gameObject.GetComponent(); + titleObj.text = title; + titleObj.transform.localPosition += titleOffset; + var subtitleObj = toast.FindChild("AchievementObtainedText").gameObject.GetComponent(); + subtitleObj.text = subtitle; + subtitleObj.transform.localPosition += subtitleOffset; } private static IEnumerator CoAnimateAndDestroyToast() @@ -74,7 +79,7 @@ private static IEnumerator CoAnimateAndDestroyToast() yield return null; } - currentToast.gameObject.DeepDestroy(); + currentToast.gameObject.Destroy(); yield break; } @@ -87,10 +92,18 @@ public static IEnumerator CoShowAndDeleteToast(BaseAchievement achievement) currentToast = GetOrCreateToast(); PopulateToast(currentToast, achievement.Icon, achievement.Assembly, - title: "Achievement Obtained!", - subtitle: achievement.Name); + "Achievement Obtained!", + achievement.Name, + achievement.ToastTitleOffset, + achievement.ToastObtainedOffset, + achievement.ToastIconOffset); + var img = currentToast.GetComponent(); + if (achievement.ToastBgSprite != null) + { + img.m_Sprite = achievement.ToastBgSprite; + } - yield return Coroutines.Start(CoAnimateAndDestroyToast()); + yield return CoAnimateAndDestroyToast(); } public static IEnumerator CoShowAndDeleteToast(CountAchievement achievement, bool unlocked = false) @@ -104,16 +117,27 @@ public static IEnumerator CoShowAndDeleteToast(CountAchievement achievement, boo if (achievement.Hidden && achievement.HideProgress && !unlocked) { PopulateToast(currentToast, achievement.Icon, achievement.Assembly, - title: "Achievement Progressed!", - subtitle: $"Hidden Achievement"); + "Achievement Progressed!", + "Hidden Achievement", + achievement.ToastTitleOffset, + achievement.ToastObtainedOffset, + achievement.ToastIconOffset); } else { PopulateToast(currentToast, achievement.Icon, achievement.Assembly, - title: unlocked ? "Achievement Obtained!" : "Achievement Progressed!", - subtitle: $"{achievement.Name} ({achievement.CurrentValue}/{achievement.RequiredValue})"); + unlocked ? "Achievement Obtained!" : "Achievement Progressed!", + $"{achievement.Name} ({achievement.CurrentValue}/{achievement.RequiredValue})", + achievement.ToastTitleOffset, + achievement.ToastObtainedOffset, + achievement.ToastIconOffset); + } + var img = currentToast.GetComponent(); + if (achievement.ToastBgSprite != null) + { + img.m_Sprite = achievement.ToastBgSprite; } - yield return Coroutines.Start(CoAnimateAndDestroyToast()); + yield return CoAnimateAndDestroyToast(); } } \ No newline at end of file diff --git a/AchievementsAPI/API/AchievementsTab.cs b/AchievementsAPI/API/AchievementsTab.cs index 70c0603..5a7844f 100644 --- a/AchievementsAPI/API/AchievementsTab.cs +++ b/AchievementsAPI/API/AchievementsTab.cs @@ -15,7 +15,7 @@ public abstract class AchievementsTab public abstract string Name { get; } /// - /// Wether the tab should be selectable in the Achievements Menu. Handy for custom UI's or internal achievements. + /// Whether the tab should be selectable in the Achievements Menu. Handy for custom UI's or internal achievements. /// public virtual bool IsSelectable => true; diff --git a/AchievementsAPI/API/BaseAchievement.cs b/AchievementsAPI/API/BaseAchievement.cs index 06e886c..ee62ddc 100644 --- a/AchievementsAPI/API/BaseAchievement.cs +++ b/AchievementsAPI/API/BaseAchievement.cs @@ -20,10 +20,73 @@ public class BaseAchievement /// The achievement's icon's path /// public string IconPath; + + /// + /// Gets or sets the achievement's icon + /// + public virtual Sprite Icon => _sprite; + + /// + /// Gets the background sprite for the achievement in the achievements menu. + /// + public virtual Sprite? MenuBgSprite => null; + + /// + /// Gets the background sprite for the achievement in the toast pop up. + /// + public virtual Sprite? ToastBgSprite => null; + + /// + /// Gets the offset for the achievement's icon in the achievements menu. + /// + public virtual Vector3 MenuIconOffset => new(0, 0); + + /// + /// Gets the offset for the achievement's title in the achievements menu. + /// + public virtual Vector3 MenuTitleOffset => new(0, 0); + + /// + /// Gets the offset for the achievement's description in the achievements menu. + /// + public virtual Vector3 MenuDescOffset => new(0, 0); + + /// + /// Gets the offset for the achievement's sub-icon, if any. + /// + public virtual Vector3 MenuSubIconOffset => new(0, 0); + /// - /// The achievement's icon + /// Gets the scale for the achievement's sub-icon, if any. /// - public Sprite Icon; + public virtual Vector3 MenuSubIconScale => new(0.5f, 0.5f, 1); + + /// + /// Gets the achievement's sub-icon, if any. + /// + public virtual Sprite? MenuSubIcon => null; + + /// + /// Gets the offset for the achievement's "Obtained" text in the toast pop up. + /// + public virtual Vector3 ToastObtainedOffset => new(0, 0); + + /// + /// Gets the offset for the achievement's title in the toast pop up. + /// + public virtual Vector3 ToastTitleOffset => new(0, 0); + + /// + /// Gets the offset for the achievement's icon in the toast pop up. + /// + public virtual Vector3 ToastIconOffset => new(0, 0); + + /// + /// Whether the achievement's background becomes colored. + /// + public bool RarityOnBgSprite = true; + + private Sprite _sprite; public bool Unlocked; /// /// The achievement's rarity: @@ -34,11 +97,11 @@ public class BaseAchievement /// public int Rarity; /// - /// Wether the achievement is hidden or not (hidden achievements get the default icon and have their name and description set to "Hidden Achievement" until unlocked) + /// Whether the achievement is hidden or not (hidden achievements get the default icon and have their name and description set to "Hidden Achievement" until unlocked) /// public bool Hidden; /// - /// Wether to hide the achievement's rarity (if the achievement is hidden) + /// Whether to hide the achievement's rarity (if the achievement is hidden) /// public bool HideRarity; public Assembly Assembly; @@ -56,15 +119,13 @@ public void Unlock(bool showOnUI = true, bool doStorageUpdate = true) if (doStorageUpdate) AchievementStorage.AchievementStorageUpdate(this, true); } - - public BaseAchievement(string name, string description, string iconPath, int rarity = 0, bool hidden = false, bool hideRarity = true, Assembly? assembly = null) { Name = name; Description = description; IconPath = iconPath; Assembly = assembly ?? Assembly.GetCallingAssembly(); - Icon = SpriteTools.LoadSpriteFromPath(IconPath, Assembly, 100); + _sprite = SpriteTools.LoadSpriteFromPath(IconPath, Assembly, 100); Id = Assembly.GetName().Name + "_" + Name; Rarity = rarity; Hidden = hidden; @@ -75,7 +136,17 @@ public BaseAchievement(string name, string description, Sprite icon, int rarity Name = name; Description = description; Assembly = assembly ?? Assembly.GetCallingAssembly(); - Icon = icon; + _sprite = icon; + Id = Assembly.GetName().Name + "_" + Name; + Rarity = rarity; + Hidden = hidden; + HideRarity = hideRarity; + } + public BaseAchievement(string name, string description, int rarity = 0, bool hidden = false, bool hideRarity = true, Assembly? assembly = null) + { + Name = name; + Description = description; + Assembly = assembly ?? Assembly.GetCallingAssembly(); Id = Assembly.GetName().Name + "_" + Name; Rarity = rarity; Hidden = hidden; diff --git a/AchievementsAPI/API/CountAchievement.cs b/AchievementsAPI/API/CountAchievement.cs index cf889fa..c0cf7d9 100644 --- a/AchievementsAPI/API/CountAchievement.cs +++ b/AchievementsAPI/API/CountAchievement.cs @@ -17,21 +17,28 @@ public class CountAchievement : BaseAchievement /// public int RequiredValue; /// - /// Defines if the progress persists between games. + /// Defines how the progress persists between games. /// - public bool ProgressPersists; + public AchPersistence ProgressPersists; /// - /// Wether to hide the achievement's progress (if the achievement is hidden) + /// Whether to hide the achievement's progress (if the achievement is hidden) /// public bool HideProgress; - public CountAchievement(string name, string description, string iconPath, int currentValue, int requiredValue, bool progressPersists = true, int rarity = 0, bool hidden = false, bool hideRarity = true, bool hideProgress = false) : base(name, description, iconPath, rarity, hidden, hideRarity, System.Reflection.Assembly.GetCallingAssembly()) + public CountAchievement(string name, string description, string iconPath, int currentValue, int requiredValue, AchPersistence progressPersists = AchPersistence.ThroughoutSessions, int rarity = 0, bool hidden = false, bool hideRarity = true, bool hideProgress = false) : base(name, description, iconPath, rarity, hidden, hideRarity, System.Reflection.Assembly.GetCallingAssembly()) { CurrentValue = currentValue; RequiredValue = requiredValue; ProgressPersists = progressPersists; HideProgress = hideProgress; } - public CountAchievement(string name, string description, Sprite icon, int currentValue, int requiredValue, bool progressPersists = true, int rarity = 0, bool hidden = false, bool hideRarity = true, bool hideProgress = false) : base(name, description, icon, rarity, hidden, hideRarity, System.Reflection.Assembly.GetCallingAssembly()) + public CountAchievement(string name, string description, Sprite icon, int currentValue, int requiredValue, AchPersistence progressPersists = AchPersistence.ThroughoutSessions, int rarity = 0, bool hidden = false, bool hideRarity = true, bool hideProgress = false) : base(name, description, icon, rarity, hidden, hideRarity, System.Reflection.Assembly.GetCallingAssembly()) + { + CurrentValue = currentValue; + RequiredValue = requiredValue; + ProgressPersists = progressPersists; + HideProgress = hideProgress; + } + public CountAchievement(string name, string description, int currentValue, int requiredValue, AchPersistence progressPersists = AchPersistence.ThroughoutSessions, int rarity = 0, bool hidden = false, bool hideRarity = true, bool hideProgress = false) : base(name, description, rarity, hidden, hideRarity, System.Reflection.Assembly.GetCallingAssembly()) { CurrentValue = currentValue; RequiredValue = requiredValue; @@ -54,7 +61,6 @@ public void Increment(int count, bool showOnUI = true) /// Shows an unlock animation on the hud. public void SetValue(int value, bool showOnUI = true) { - CurrentValue = value; if (showOnUI && !Unlocked) AchievementToast.ShowAndDeleteToast(this, Unlocked); if (CurrentValue >= RequiredValue) @@ -67,4 +73,12 @@ public void SetValue(int value, bool showOnUI = true) AchievementStorage.AchievementStorageUpdate(this, value, Unlocked); } +} + +public enum AchPersistence +{ + ThroughoutSessions, + ThroughoutRounds, + ResetOnRoundStart, + ResetOnMeetingStart } \ No newline at end of file diff --git a/AchievementsAPI/AchievementsAPI.csproj b/AchievementsAPI/AchievementsAPI.csproj index 81dd53e..4381a12 100644 --- a/AchievementsAPI/AchievementsAPI.csproj +++ b/AchievementsAPI/AchievementsAPI.csproj @@ -7,11 +7,19 @@ 1.1.3 dev Achievements API for Among Us + true + LICENSE + true - + + + + runtime; compile; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/AchievementsAPI/AchievementsAPIPlugin.cs b/AchievementsAPI/AchievementsAPIPlugin.cs index cfa0d50..3b8418d 100644 --- a/AchievementsAPI/AchievementsAPIPlugin.cs +++ b/AchievementsAPI/AchievementsAPIPlugin.cs @@ -4,14 +4,13 @@ using BepInEx.Configuration; using BepInEx.Unity.IL2CPP; using HarmonyLib; -using Reactor; -using Reactor.Utilities; +using VentLib; namespace AchievementsAPI; [BepInAutoPlugin] [BepInProcess("Among Us.exe")] -[BepInDependency(ReactorPlugin.Id)] +[BepInDependency(Vents.Id)] public partial class AchievementsAPIPlugin : BasePlugin { public Harmony Harmony { get; } = new(Id); diff --git a/AchievementsAPI/Assets.cs b/AchievementsAPI/Assets.cs index d215ae9..b062e65 100644 --- a/AchievementsAPI/Assets.cs +++ b/AchievementsAPI/Assets.cs @@ -1,16 +1,26 @@ +using System; using System.Reflection; -using Reactor.Utilities; -using Reactor.Utilities.Extensions; +using AchievementsAPI.Reactor.Embedded; using UnityEngine; namespace AchievementsAPI; public class Assets { - public static AssetBundle assetBundle { get; set; } = AssetBundleManager.Load("achievements"); - public static GameObject achievementPrefab { get; set; } = assetBundle.LoadAsset("AchievementsMenu").DontDestroy().DontDestroyOnLoad().DontDestroyOnLoad(); - public static GameObject achievementToastCanvasPrefab { get; set; } = assetBundle.LoadAsset("ToastCanvas").DontDestroy().DontDestroyOnLoad().DontDestroyOnLoad(); - public static GameObject achievementToastPrefab { get; set; } = assetBundle.LoadAsset("Toast").DontDestroy().DontDestroyOnLoad().DontDestroyOnLoad(); - public static Sprite StarSprite { get; set; } = assetBundle.LoadAsset("Star"); + public static AssetBundle assetBundle { get; set; } = EmbeddedBundleManager.Load("achievements"); + public static GameObject achievementPrefab { get; set; } = + assetBundle.LoadAsset("AchievementsMenu") + ?.DontDestroy() + ?? throw new InvalidOperationException("Asset 'AchievementsMenu' not found in bundle"); + + public static GameObject achievementToastCanvasPrefab { get; set; } = + assetBundle.LoadAsset("ToastCanvas")? + .DontDestroy() + ?? throw new InvalidOperationException("Asset 'ToastCanvas' not found in bundle"); + public static GameObject achievementToastPrefab { get; set; } = + assetBundle.LoadAsset("Toast")? + .DontDestroy() + ?? throw new InvalidOperationException("Asset 'Toast' not found in bundle"); + public static Sprite StarSprite { get; set; } = assetBundle.LoadAsset("Star"); } \ No newline at end of file diff --git a/AchievementsAPI/Components/AchievementsMenu.cs b/AchievementsAPI/Components/AchievementsMenu.cs index cd3b036..0828491 100644 --- a/AchievementsAPI/Components/AchievementsMenu.cs +++ b/AchievementsAPI/Components/AchievementsMenu.cs @@ -1,16 +1,17 @@ -using System; using System.Collections; using System.Collections.Generic; -using System.Linq; +using System.Globalization; using System.Reflection; +using System; +using System.Linq; using AchievementsAPI.API; using Il2CppInterop.Runtime.InteropTypes.Fields; -using Reactor.Utilities; -using Reactor.Utilities.Attributes; -using Reactor.Utilities.Extensions; using TMPro; using UnityEngine; using UnityEngine.UI; +using VentLib.Utilities; +using VentLib.Utilities.Attributes; +using VentLib.Utilities.Extensions; using Object = UnityEngine.Object; namespace AchievementsAPI @@ -27,6 +28,7 @@ public class AchievementsMenu(IntPtr ptr) : MonoBehaviour(ptr) public Il2CppReferenceField percentageText; public Il2CppReferenceField progressBar; public MainMenuManager mainMenuManager; + public OptionsMenuBehaviour OptionsMenuBehaviour; public List items = new(); private void Start() { @@ -40,20 +42,38 @@ private void Start() btn.onClick.AddListener(new Action(() => { SetTab(tab); - mainMenuManager?.StartCoroutine( - Effects.ActionAfterDelay(0.01f, new System.Action(() => SetTab(tab)))); + if (mainMenuManager) + { + mainMenuManager.StartCoroutine( + Effects.ActionAfterDelay(0.01f, new System.Action(() => SetTab(tab)))); + } + else if (OptionsMenuBehaviour) + { + OptionsMenuBehaviour.gameObject.SetActive(true); + OptionsMenuBehaviour.StartCoroutine(Effects.ActionAfterDelay(0.01f, new System.Action(() => SetTab(tab, true)))); + } })); AchievementStorage.AchievementStorageGet(tab); } - SetTab(AchievementsManager.Tabs[0]); - mainMenuManager?.DeactivateMainMenuUI(); + + var firstTab = AchievementsManager.Tabs.FirstOrDefault(x => x.IsSelectable) ?? AchievementsManager.Tabs[0]; + SetTab(firstTab); + if (mainMenuManager) + { + mainMenuManager.DeactivateMainMenuUI(); + } + else if (OptionsMenuBehaviour) + { + OptionsMenuBehaviour.gameObject.SetActive(false); + OptionsMenuBehaviour.Background.enabled = false; + } } - private void SetTab(AchievementsTab tab) + private void SetTab(AchievementsTab tab, bool inOptionsMenu = false) { foreach (var element in items) { - element.gameObject.DeepDestroy(); + element.gameObject.Destroy(); } items = new(); @@ -80,29 +100,52 @@ private void SetTab(AchievementsTab tab) uiElement.descriptionText.Value.text += $" ({countAchievement.CurrentValue}/{countAchievement.RequiredValue})"; } - if (achievement.HideRarity && !achievement.Unlocked && achievement.Hidden) - { - - } - else if (achievement.Rarity == 1) + uiElement.nameText.Value.transform.localPosition += achievement.MenuTitleOffset; + uiElement.descriptionText.Value.transform.localPosition += achievement.MenuDescOffset; + uiElement.grayscaleImage.Value.transform.localPosition += achievement.MenuIconOffset; + uiElement.iconImage.Value.transform.localPosition += achievement.MenuIconOffset; + if (achievement.MenuSubIcon != null) { - uiElement.GetComponent().color = new Color32(112, 208, 255, 255); + var subIcon = Instantiate(uiElement.iconImage.Value, uiElement.iconImage.Value.transform.parent); + subIcon.fillAmount = 1; + subIcon.m_Sprite = achievement.MenuSubIcon; + subIcon.transform.localScale = achievement.MenuSubIconScale; + subIcon.transform.localPosition += achievement.MenuSubIconOffset; } - else if (achievement.Rarity == 2) + + var img = uiElement.GetComponent(); + if (achievement.MenuBgSprite != null) { - uiElement.GetComponent().color = new Color32(187, 88, 255, 255); + img.m_Sprite = achievement.MenuBgSprite; } - else if (achievement.Rarity == 3) + if (achievement.RarityOnBgSprite && !(!achievement.HideRarity && !achievement.Unlocked && achievement.Hidden)) { - uiElement.GetComponent().color = new Color32(255, 226, 64, 255); + if (achievement.Rarity == 1) + { + img.color = new Color32(112, 208, 255, 255); + } + else if (achievement.Rarity == 2) + { + img.color = new Color32(187, 88, 255, 255); + } + else if (achievement.Rarity == 3) + { + img.color = new Color32(255, 226, 64, 255); + } } + achievementCount++; if (achievement.Unlocked) completedAchievementCount++; items.Add(uiElement); } + + if (inOptionsMenu && OptionsMenuBehaviour) + { + OptionsMenuBehaviour.gameObject.SetActive(false); + } if (!transform.GetChild(0).TryGetComponent(out var image)) return; - Coroutines.Start(FadeColor(image, image.color, tab.GetTabColor(), 0.3f)); + Async.Execute(FadeColor(image, image.color, tab.GetTabColor(), 0.3f)); progressBar.Value.fillMethod = Image.FillMethod.Horizontal; if (achievementCount != 0 || completedAchievementCount != 0) progressBar.Value.fillAmount = (float) completedAchievementCount / achievementCount; else @@ -110,14 +153,23 @@ private void SetTab(AchievementsTab tab) progressBar.Value.fillAmount = 0; return; } - - percentageText.Value.text = $"{(double) completedAchievementCount / achievementCount * 100}%"; + + double percent = (double) completedAchievementCount / achievementCount * 100f; + percentageText.Value.text = $"{percent.ToString("0.00", CultureInfo.InvariantCulture)}%"; } public void Close() { - mainMenuManager?.ActivateMainMenuUI(); - gameObject.DeepDestroy(); + if (mainMenuManager) + { + mainMenuManager.ActivateMainMenuUI(); + } + else if (OptionsMenuBehaviour) + { + OptionsMenuBehaviour.gameObject.SetActive(true); + OptionsMenuBehaviour.Background.enabled = true; + } + gameObject.Destroy(); } public void OnSearchbarChanged(string val) { diff --git a/AchievementsAPI/Components/AchievementsMenuItem.cs b/AchievementsAPI/Components/AchievementsMenuItem.cs index 0198acc..65bc3fd 100644 --- a/AchievementsAPI/Components/AchievementsMenuItem.cs +++ b/AchievementsAPI/Components/AchievementsMenuItem.cs @@ -1,16 +1,17 @@ +using System; using System.Collections; using System.Collections.Generic; using Il2CppInterop.Runtime.InteropTypes.Arrays; using Il2CppInterop.Runtime.InteropTypes.Fields; -using Reactor.Utilities.Attributes; using TMPro; using UnityEngine; using UnityEngine.UI; +using VentLib.Utilities.Attributes; namespace AchievementsAPI { [RegisterInIl2Cpp] - public class AchievementsMenuItem : MonoBehaviour + public class AchievementsMenuItem(IntPtr ptr) : MonoBehaviour(ptr) { public Il2CppReferenceField nameText; public Il2CppReferenceField descriptionText; diff --git a/AchievementsAPI/ExampleTab.cs b/AchievementsAPI/ExampleTab.cs index f379acd..0edf859 100644 --- a/AchievementsAPI/ExampleTab.cs +++ b/AchievementsAPI/ExampleTab.cs @@ -7,7 +7,7 @@ namespace AchievementsAPI; public class ExampleTab : AchievementsTab { public override string Name => "Example Tab"; - public override bool IsSelectable => true; + public override bool IsSelectable => AchievementsManager.Tabs.Count == 1; // if there are more tabs, those will be shown instead. public override Color GetTabColor() { return Color.red; diff --git a/AchievementsAPI/Extensions.cs b/AchievementsAPI/Extensions.cs index 0f9894d..8373da7 100644 --- a/AchievementsAPI/Extensions.cs +++ b/AchievementsAPI/Extensions.cs @@ -1,8 +1,8 @@ using System; using System.Collections; -using Reactor.Utilities; -using Reactor.Utilities.Extensions; using UnityEngine; +using VentLib.Utilities; +using VentLib.Utilities.Extensions; // Completely stole this from MiraAPI, thanks hehe @@ -22,7 +22,7 @@ public static void DeepDestroy(this GameObject obj, bool clearGc = true) var needsDeepDestroy = version >= requiredVersion; if (needsDeepDestroy) { - Coroutines.Start(Nuke(obj, clearGc)); + Async.Execute(Nuke(obj, clearGc)); } else { @@ -126,7 +126,7 @@ private static IEnumerator Nuke(GameObject? go, bool clearGc) /// public static void ClearGarbageCollector() { - Coroutines.Start(CoFreeResources()); + Async.Execute(CoFreeResources()); } private static IEnumerator CoFreeResources() diff --git a/AchievementsAPI/Patches/AchievementPersistencePatches.cs b/AchievementsAPI/Patches/AchievementPersistencePatches.cs new file mode 100644 index 0000000..a32ea82 --- /dev/null +++ b/AchievementsAPI/Patches/AchievementPersistencePatches.cs @@ -0,0 +1,72 @@ +using System.Linq; +using System.Collections.Generic; +using AchievementsAPI.API; +using HarmonyLib; + +namespace AchievementsAPI.Patches; + +// For resetting achievements that don't persist + +[HarmonyPatch] +public static class AchievementPersistencePatches +{ + [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.OnGameEnd))] + [HarmonyPatch(typeof(AchievementManager), nameof(AchievementManager.OnMatchStart))] + [HarmonyPatch(typeof(AchievementManager), nameof(AchievementManager.OnMatchExited))] + [HarmonyPatch(typeof(LobbyBehaviour), nameof(LobbyBehaviour.Start))] + [HarmonyPatch(typeof(TutorialManager), nameof(TutorialManager.Awake))] + [HarmonyPrefix] + public static void OnGameEndAndStart() + { + foreach (var achievement in GetIncompleteAchievements()) + { + if (achievement.ProgressPersists != AchPersistence.ThroughoutSessions) + { + AchievementStorage.AchievementDataReset(achievement); + } + } + } + [HarmonyPatch(typeof(AchievementManager), nameof(AchievementManager.OnMeetingCalled))] + [HarmonyPrefix] + public static void OnMeetingCalled() + { + foreach (var achievement in GetIncompleteAchievements()) + { + if (achievement.ProgressPersists == AchPersistence.ResetOnMeetingStart) + { + AchievementStorage.AchievementDataReset(achievement); + } + } + } + [HarmonyPatch(typeof(ExileController), nameof(ExileController.ReEnableGameplay))] + [HarmonyPrefix] + public static void OnRoundStart() + { + foreach (var achievement in GetIncompleteAchievements()) + { + if (achievement.ProgressPersists == AchPersistence.ResetOnRoundStart) + { + AchievementStorage.AchievementDataReset(achievement); + } + } + } + + private static List GetIncompleteAchievements() + { + var newAchievements = new List(); + foreach (var tab in AchievementsManager.Tabs) + { + foreach (var propInfo in tab.GetType().GetProperties().Where(x => x.PropertyType == typeof(CountAchievement))) + { + var achievement = propInfo.GetValue(tab) as CountAchievement; + if (achievement == null) continue; + if (achievement.CurrentValue != achievement.RequiredValue) + { + newAchievements.Add(achievement); + } + } + } + + return newAchievements; + } +} \ No newline at end of file diff --git a/AchievementsAPI/Patches/MainMenu/AchievementsMenuOpen.cs b/AchievementsAPI/Patches/MainMenu/AchievementsMenuOpen.cs index 7234c3c..c217de3 100644 --- a/AchievementsAPI/Patches/MainMenu/AchievementsMenuOpen.cs +++ b/AchievementsAPI/Patches/MainMenu/AchievementsMenuOpen.cs @@ -1,15 +1,19 @@ -using AchievementsAPI; -using AchievementsAPI.API; -using UnityEngine; +using UnityEngine; -namespace AchievementsAPI.MainMenu; +namespace AchievementsAPI.Patches.MainMenu; public class AchievementsMenuOpen { public static void OpenMenu(MainMenuManager mainMenuManager) { - var menu = Object.Instantiate(Assets.achievementPrefab).GetComponent(); + var menu = UnityEngine.Object.Instantiate(Assets.achievementPrefab).GetComponent(); menu.mainMenuManager = mainMenuManager; menu.gameObject.SetActive(true); } + public static void OpenMenu(OptionsMenuBehaviour optionsMenuBehaviour) + { + var menu = UnityEngine.Object.Instantiate(Assets.achievementPrefab).GetComponent(); + menu.OptionsMenuBehaviour = optionsMenuBehaviour; + menu.gameObject.SetActive(true); + } } \ No newline at end of file diff --git a/AchievementsAPI/Patches/MainMenu/MainMenuPatch.cs b/AchievementsAPI/Patches/MainMenu/MainMenuPatch.cs index b623701..1127b21 100644 --- a/AchievementsAPI/Patches/MainMenu/MainMenuPatch.cs +++ b/AchievementsAPI/Patches/MainMenu/MainMenuPatch.cs @@ -16,7 +16,15 @@ public static void OnMainMenuAwakePostfix(MainMenuManager __instance) { AchievementStorage.Load(); _storageInitialized = true; + foreach (var tab in AchievementsManager.Tabs) + { + AchievementStorage.AchievementStorageGet(tab); + } + + if (AchievementsManager.Tabs.Count == 1) + { + AchievementsTabSingleton.Instance.achievement.Unlock(); + } } - AchievementsTabSingleton.Instance.achievement.Unlock(); } } \ No newline at end of file diff --git a/AchievementsAPI/Patches/OptionsMenuPatches.cs b/AchievementsAPI/Patches/OptionsMenuPatches.cs new file mode 100644 index 0000000..e156865 --- /dev/null +++ b/AchievementsAPI/Patches/OptionsMenuPatches.cs @@ -0,0 +1,103 @@ +using System.Reflection; +using AchievementsAPI.Patches.MainMenu; +using HarmonyLib; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using VentLib.Utilities.Extensions; +using Object = UnityEngine.Object; + +namespace AchievementsAPI; + +[HarmonyPatch(typeof(OptionsMenuBehaviour))] +public static class OptionsMenuPatches +{ + internal static OptionsMenuBehaviour? Instance { get; private set; } + internal static BoxCollider2D MaskCollider = null!; + private static SpriteRenderer? background; + + private static int currentPage = 1; + + [HarmonyPostfix] + [HarmonyPatch(nameof(OptionsMenuBehaviour.Start))] + public static void StartPostfix(OptionsMenuBehaviour __instance) + { + Instance = __instance; + background = __instance.Background; + // Fix for tabs not being clickable in the main menu + if (!AmongUsClient.Instance.IsInGame) + { + return; + } + + var maskObj = new GameObject + { + layer = 5, + name = "SpriteMask", + }; + maskObj.transform.SetParent(__instance.transform); + maskObj.transform.localPosition = new Vector3(0, -0.3f, 0); + maskObj.transform.localScale = new Vector3(500, 120, 1); + + var blank = Sprite.Create( + Texture2D.whiteTexture, + new Rect(0, 0, Texture2D.whiteTexture.width, Texture2D.whiteTexture.height), + new Vector2(0.5f, 0.5f)); + + var mask = maskObj.AddComponent(); + mask.sprite = blank; + mask.isCustomRangeActive = true; + mask.sortingLayerName = "Default"; + mask.backSortingOrder = 100; + mask.frontSortingOrder = 500; + + MaskCollider = maskObj.AddComponent(); + MaskCollider.size = new Vector2(0.01f, 0.1f); + MaskCollider.isTrigger = true; + MaskCollider.enabled = true; + + currentPage = 1; + float yOffset = 0; + int i = 0; + int tabIdx = 0; + int page = 1; + var button = CreateTabButton(__instance, ref tabIdx, ref yOffset); + } + + /// + /// Creates the tab button. + /// + /// The instance. + /// The tab index. + /// The current button offset. + /// The created button . + public static GameObject CreateTabButton(OptionsMenuBehaviour instance, ref int tabIdx, ref float offset) + { + var tabButtonObject = Object.Instantiate(instance.Tabs[0], instance.transform); + tabButtonObject.name = $"Achievements Button"; + tabButtonObject.transform.localPosition = new Vector3(-2.4f, 2.1f - offset, 5.5f); + tabButtonObject.transform.localScale = new Vector3(1.25f, 1.25f, 1); + var color = new Color32(150, 150, 50, 255); + var colorHover = new Color32(200, 200, 100, 255); + tabButtonObject.Button.color = color; + + var tabButtonText = tabButtonObject.transform.FindChild("Text_TMP").GetComponent(); + tabButtonText.gameObject.Destroy(); + + var tabButton = tabButtonObject.GetComponent(); + var rollover = tabButtonObject.Rollover; + rollover.OverColor = colorHover; + rollover.OutColor = color; + + var tabButtonRend = new GameObject("sprite").AddComponent(); + tabButtonRend.gameObject.layer = 5; // ui layer + tabButtonRend.transform.SetParent(tabButtonObject.transform); + tabButtonRend.transform.localPosition = new Vector3(-0.5f, 0, -2); + tabButtonRend.transform.localScale = new Vector3(0.2f, 0.2f, 1); + tabButtonRend.sprite = SpriteTools.LoadSpriteFromPath("AchievementsAPI.Resources.AchievementsIcon.png", Assembly.GetCallingAssembly(), 100); + + tabButton.OnClick.AddListener((UnityAction)(() => { AchievementsMenuOpen.OpenMenu(instance); })); + + return tabButtonObject.gameObject; + } +} \ No newline at end of file diff --git a/AchievementsAPI/Patches/PlayerControlPatches.cs b/AchievementsAPI/Patches/PlayerControlPatches.cs deleted file mode 100644 index c196a9a..0000000 --- a/AchievementsAPI/Patches/PlayerControlPatches.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Linq; -using AchievementsAPI.API; -using HarmonyLib; - -namespace AchievementsAPI.Patches; - -// For resetting achievements that don't persist - -[HarmonyPatch] -public class PlayerControlPatches -{ - [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.OnGameEnd))] - [HarmonyPrefix] - public static void PlayerControl_OnGameEnd_Prefix(PlayerControl __instance) - { - foreach (var tab in AchievementsManager.Tabs) - { - foreach (var propInfo in tab.GetType().GetProperties().Where(x => x.PropertyType == typeof(CountAchievement))) - { - var achievement = (CountAchievement) propInfo.GetValue(tab); - if (achievement == null) continue; - if (!achievement.ProgressPersists) achievement.CurrentValue = 0; - } - } - } -} \ No newline at end of file diff --git a/AchievementsAPI/Reactor.Embedded/EmbeddedBundleManager.cs b/AchievementsAPI/Reactor.Embedded/EmbeddedBundleManager.cs new file mode 100644 index 0000000..d528494 --- /dev/null +++ b/AchievementsAPI/Reactor.Embedded/EmbeddedBundleManager.cs @@ -0,0 +1,216 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using UnityEngine; +using VentLib.Logging; + +namespace AchievementsAPI.Reactor.Embedded; + +/// +/// Provides a standard way of loading asset bundles from a file or embedded resource. +/// +// https://github.com/NuclearPowered/Reactor/blob/04100970b66d57a2dd96f6f4eae0383362e9a321/Reactor/Utilities/AssetBundleManager.cs +public static class EmbeddedBundleManager +{ + private static readonly StandardLogger log = LoggerFactory.GetLogger(typeof(EmbeddedBundleManager)); + + /// + /// Extension of a asset bundle. + /// + public const string BundleExtension = ".bundle"; + + /// + /// Gets the target name of the current system. + /// + [Obsolete("Use AssetBundleManager#GetTargetName instead")] + public static string TargetName { get; } = GetTargetName(true); + + /// + /// Gets the target name of the current system. + /// + /// A value indicating whether to include the process architecture. + /// Target name of the current system. + public static string GetTargetName(bool includeArchitecture) + { + string operatingSystem; + + if (OperatingSystem.IsWindows()) + { + operatingSystem = "win"; + } + else if (OperatingSystem.IsLinux()) + { + operatingSystem = "linux"; + } + else if (OperatingSystem.IsAndroid()) + { + operatingSystem = "android"; + } + else + { + throw new PlatformNotSupportedException(); + } + + if (!includeArchitecture) + { + return operatingSystem; + } + + return $"{operatingSystem}-{RuntimeInformation.ProcessArchitecture switch + { + Architecture.X86 => "x86", + Architecture.X64 => "x64", + Architecture.Arm => "arm", + Architecture.Arm64 => "arm64", + _ => throw new PlatformNotSupportedException(), + }}"; + } + + /// + /// Gets the file name for an asset bundle named . + /// + /// The name of the asset bundle. + /// File name of the asset bundle. + [Obsolete("Use the overload with includeArchitecture instead")] + public static string GetFileName(string assetBundleName) => GetFileName(assetBundleName, true); + + /// + /// Gets the file name for an asset bundle named . + /// + /// The name of the asset bundle. + /// A value indicating whether to include the process architecture. + /// File name of the asset bundle. + public static string GetFileName(string assetBundleName, bool includeArchitecture) => $"{assetBundleName}-{GetTargetName(includeArchitecture)}{BundleExtension}"; + + private static bool TryFindFile(Assembly assembly, string fileName, [NotNullWhen(true)] out string? path) + { + var pluginDirectoryPath = Path.GetDirectoryName(assembly.Location); + if (pluginDirectoryPath != null) + { + var filePath = Path.Combine(pluginDirectoryPath, fileName); + if (File.Exists(filePath)) + { + log.Debug($"Loading an asset bundle from {filePath}"); + path = filePath; + return true; + } + } + + path = null; + return false; + } + + private static bool TryLoadResource(Assembly assembly, string fileName, [NotNullWhen(true)] out Il2CppStructArray? data) + { + var resourceName = assembly.GetManifestResourceNames().SingleOrDefault(n => n.EndsWith(fileName, StringComparison.Ordinal)); + if (resourceName != null) + { + log.Debug($"Loading an asset bundle from {resourceName}"); + + using var stream = assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException("Resource stream was null"); + + var length = (int) stream.Length; + data = new Il2CppStructArray(length); + if (stream.Read(data.ToSpan()) < length) throw new IOException("Failed to read in full"); + + return true; + } + + data = null; + return false; + } + + /// + /// Synchronously loads an named from the calling assembly. + /// + /// The name of the asset bundle. + /// The loaded assetbundle. + /// Couldn't find an assetbundle named . + public static AssetBundle Load(string name) + { + return Load(Assembly.GetCallingAssembly(), name); + } + + /// + /// Synchronously loads an named from the specified . + /// + /// The specified assembly. + /// The name of the asset bundle. + /// The loaded assetbundle. + /// Couldn't find an assetbundle named . + public static AssetBundle Load(Assembly assembly, string name) + { + return TryLoad(assembly, GetFileName(name, includeArchitecture: true)) + ?? TryLoad(assembly, GetFileName(name, includeArchitecture: false)) + ?? throw new AssetBundleNotFoundException(name); + } + + private static AssetBundle? TryLoad(Assembly assembly, string fileName) + { + if (TryFindFile(assembly, fileName, out var filePath)) + { + return AssetBundle.LoadFromFile(filePath); + } + + if (TryLoadResource(assembly, fileName, out var data)) + { + return AssetBundle.LoadFromMemory(data); + } + + return null; + } + + /// + /// Asynchronously loads an named from the calling assembly. + /// + /// The name of the asset bundle. + /// The loaded assetbundle. + /// Couldn't find an assetbundle named . + public static AssetBundleCreateRequest LoadAsync(string name) + { + return LoadAsync(Assembly.GetCallingAssembly(), name); + } + + /// + /// Asynchronously loads an named from the specified . + /// + /// The specified assembly. + /// The name of the asset bundle. + /// The loaded assetbundle. + /// Couldn't find an assetbundle named . + public static AssetBundleCreateRequest LoadAsync(Assembly assembly, string name) + { + return TryLoadAsync(assembly, GetFileName(name, includeArchitecture: true)) + ?? TryLoadAsync(assembly, GetFileName(name, includeArchitecture: false)) + ?? throw new AssetBundleNotFoundException(name); + } + + private static AssetBundleCreateRequest? TryLoadAsync(Assembly assembly, string fileName) + { + if (TryFindFile(assembly, fileName, out var filePath)) + { + return AssetBundle.LoadFromFileAsync(filePath); + } + + if (TryLoadResource(assembly, fileName, out var data)) + { + return AssetBundle.LoadFromMemoryAsync(data); + } + + return null; + } +} + +/// +/// The exception that is thrown when an assetbundle is not found. +/// +public class AssetBundleNotFoundException : IOException +{ + internal AssetBundleNotFoundException(string name) : base("Couldn't find an assetbundle named " + name) + { + } +} \ No newline at end of file diff --git a/AchievementsAPI/Reactor.Embedded/Extensions.cs b/AchievementsAPI/Reactor.Embedded/Extensions.cs new file mode 100644 index 0000000..4d7618b --- /dev/null +++ b/AchievementsAPI/Reactor.Embedded/Extensions.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using Il2CppInterop.Runtime; +using Il2CppInterop.Runtime.InteropTypes.Arrays; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace AchievementsAPI.Reactor.Embedded; + +public static class Extensions +{ + /// + /// Fully reads the stream. + /// + /// The stream to read. + /// A byte array read from the . + // https://github.com/NuclearPowered/Reactor/blob/04100970b66d57a2dd96f6f4eae0383362e9a321/Reactor/Utilities/Extensions/StreamExtensions.cs#L125 + public static byte[] ReadFully(this Stream input) + { + using var ms = new MemoryStream(); + input.CopyTo(ms); + return ms.ToArray(); + } + + /// + /// Loads an asset with from the with the specified type. + /// + /// The to load the asset from. + /// The name of the asset. + /// The type of the asset. + /// The loaded asset or null if it wasn't found. + // https://github.com/NuclearPowered/Reactor/blob/04100970b66d57a2dd96f6f4eae0383362e9a321/Reactor/Utilities/Extensions/AssetBundleExtensions.cs#L18 + public static T? LoadAsset(this AssetBundle bundle, string name) where T : Object + { + return bundle.LoadAsset(name, Il2CppType.Of())?.Cast(); + } + + /// + /// Stops from being destroyed. + /// + /// The object to stop from being destroyed. + /// The type of the object. + /// Passed . + // https://github.com/NuclearPowered/Reactor/blob/04100970b66d57a2dd96f6f4eae0383362e9a321/Reactor/Utilities/Extensions/UnityObjectExtensions.cs + public static T DontDestroy(this T obj) where T : Object + { + obj.hideFlags |= HideFlags.HideAndDontSave; + + return obj.DontDestroyOnLoad(); + } + + /// + /// Stops from being destroyed on load. + /// + /// The object to stop from being destroyed on load. + /// The type of the object. + /// Passed . + // https://github.com/NuclearPowered/Reactor/blob/04100970b66d57a2dd96f6f4eae0383362e9a321/Reactor/Utilities/Extensions/UnityObjectExtensions.cs + public static T DontDestroyOnLoad(this T obj) where T : Object + { + Object.DontDestroyOnLoad(obj); + + return obj; + } + + /// + /// Creates a span over a . + /// + /// The array to create a span over. + /// The type of items in the . + /// A span. + // https://github.com/NuclearPowered/Reactor/blob/04100970b66d57a2dd96f6f4eae0383362e9a321/Reactor/Utilities/Extensions/Il2CppInteropExtensions.cs#L17 + public static unsafe Span ToSpan(this Il2CppStructArray array) where T : unmanaged + { + return new Span(IntPtr.Add(array.Pointer, IntPtr.Size * 4).ToPointer(), array.Length); + } +} \ No newline at end of file diff --git a/AchievementsAPI/Resources/AchievementsIcon.png b/AchievementsAPI/Resources/AchievementsIcon.png new file mode 100644 index 0000000..0d17555 Binary files /dev/null and b/AchievementsAPI/Resources/AchievementsIcon.png differ diff --git a/AchievementsAPI/SpriteTools.cs b/AchievementsAPI/SpriteTools.cs index 63bdc70..1a62193 100644 --- a/AchievementsAPI/SpriteTools.cs +++ b/AchievementsAPI/SpriteTools.cs @@ -1,6 +1,6 @@ using System; using System.Reflection; -using Reactor.Utilities.Extensions; +using AchievementsAPI.Reactor.Embedded; using UnityEngine; namespace AchievementsAPI; diff --git a/VFC/VentFrameworkContinued.dll b/VFC/VentFrameworkContinued.dll new file mode 100644 index 0000000..528905d Binary files /dev/null and b/VFC/VentFrameworkContinued.dll differ