diff --git a/S1API/Console/CustomConsoleRegistry.cs b/S1API/Console/CustomConsoleRegistry.cs index 372bcc3..8b5be8f 100644 --- a/S1API/Console/CustomConsoleRegistry.cs +++ b/S1API/Console/CustomConsoleRegistry.cs @@ -11,7 +11,7 @@ internal static class CustomConsoleRegistry { private static readonly Logging.Log Logger = new Logging.Log("Console"); - private static readonly Dictionary registry = new Dictionary(StringComparer.OrdinalIgnoreCase); + internal static readonly Dictionary registry = new Dictionary(StringComparer.OrdinalIgnoreCase); internal static void Register(BaseConsoleCommand command) { diff --git a/S1API/Entities/Behaviour/CombatBehaviour.cs b/S1API/Entities/Behaviour/CombatBehaviour.cs index 55f01d0..d5572ee 100644 --- a/S1API/Entities/Behaviour/CombatBehaviour.cs +++ b/S1API/Entities/Behaviour/CombatBehaviour.cs @@ -13,6 +13,7 @@ using UnityEngine; using MelonLoader; using S1API.Entities.Equippables; +using S1API.Internal.Utils; using Object = UnityEngine.Object; namespace S1API.Entities.Behaviour; @@ -22,6 +23,8 @@ namespace S1API.Entities.Behaviour; /// public class CombatBehaviour { + private static readonly Logging.Log Logger = new("CombatBehaviour"); + /// /// INTERNAL: NPC reference /// @@ -57,8 +60,13 @@ public float GiveUpTime /// /// Gets or sets the default weapon asset path for the NPC's combat behaviour. /// This property allows you to specify the weapon that the NPC will use by default. - /// for convenience when setting this property. + /// Use or for convenience when setting this property. + /// Set to an empty string to clear the default weapon. /// + /// + /// Using type-safe + /// or is generally preferred and advised. + /// public string DefaultWeaponAssetPath { get @@ -74,21 +82,27 @@ public string DefaultWeaponAssetPath return; } - var go = Resources.Load(value) as GameObject; - if (go == null) + var go = Resources.Load(value); + if (!CrossType.Is(go, out var gameObject) || gameObject == null) { - Debug.LogError("Could not find weapon at path: " + value); + Logger.Error("Could not find weapon at path: " + value); return; } - var equippable = Object.Instantiate(go).GetComponent(); + var equippable = Object.Instantiate(gameObject).GetComponent(); if (equippable == null) { - Debug.LogError("Could not get AvatarEquippable from weapon at path: " + value); + Logger.Error("Could not get AvatarEquippable from weapon at path: " + value); + return; + } + + if (!CrossType.Is(equippable, out var avatarWeapon)) + { + Logger.Error("AvatarEquippable at path is not an AvatarWeapon: " + value); return; } - NPC.S1NPC.Behaviour.CombatBehaviour.DefaultWeapon = equippable as AvatarWeapon; + NPC.S1NPC.Behaviour.CombatBehaviour.DefaultWeapon = avatarWeapon; } } diff --git a/S1API/Entities/Employees/EmployeeManager.cs b/S1API/Entities/Employees/EmployeeManager.cs new file mode 100644 index 0000000..3d75657 --- /dev/null +++ b/S1API/Entities/Employees/EmployeeManager.cs @@ -0,0 +1,88 @@ +#if IL2CPPMELON +using S1Employees = Il2CppScheduleOne.Employees; +#elif MONOMELON +using S1Employees = ScheduleOne.Employees; +#endif +using System.Collections.Generic; +using S1API.Avatar; +using UnityEngine; + +namespace S1API.Entities.Employees +{ + /// + /// Provides methods for managing employee appearances and related data. + /// + public static class EmployeeManager + { + private static readonly Logging.Log Logger = new("EmployeeManager"); + + /// + /// Gets an employee appearance by index. + /// + /// Whether to choose from male employee appearance pool + /// The index of the appearance to retrieve + /// An representing the employee appearance at the specified index if successful; otherwise, null. + public static EmployeeAppearance? GetAppearance(bool male, int index) + { + if (!S1Employees.EmployeeManager.InstanceExists) + { + Logger.Error("EmployeeManager instance does not exist; cannot get appearance"); + return null; + } + + return new EmployeeAppearance(S1Employees.EmployeeManager.Instance.GetAppearance(male, index)); + } + + /// + /// Gets a random employee appearance. + /// + /// Whether to choose from male employee appearance pool + /// The index of the appearance that was retrieved + /// The avatar settings of the appearance that was retrieved + /// True if an appearance was successfully retrieved; otherwise, false. + public static bool GetRandomAppearance(bool male, out int index, out AvatarSettings? settings) + { + if (!S1Employees.EmployeeManager.InstanceExists) + { + settings = null; + index = -1; + Logger.Error("EmployeeManager instance does not exist; cannot get random appearance"); + return false; + } + + S1Employees.EmployeeManager.Instance.GetRandomAppearance(male, out var i, out var avatarSettings); + index = i; + settings = new AvatarSettings(avatarSettings); + return true; + } + } + + /// + /// Represents an employee appearance, including avatar settings and mugshot sprite. + /// + public class EmployeeAppearance + { + /// + /// INTERNAL: The underlying employee appearance from the base game. + /// + internal S1Employees.EmployeeManager.EmployeeAppearance S1EmployeeAppearance; + + /// + /// Gets the avatar settings associated with this employee appearance. + /// + public AvatarSettings Settings => new(S1EmployeeAppearance.Settings); + + /// + /// Gets the mugshot sprite associated with this employee appearance. + /// + public Sprite Mugshot => S1EmployeeAppearance.Mugshot; + + /// + /// INTERNAL: Initializes a new instance of the EmployeeAppearance class with the specified base game type appearance. + /// + internal EmployeeAppearance(S1Employees.EmployeeManager.EmployeeAppearance s1EmployeeAppearance) + { + S1EmployeeAppearance = s1EmployeeAppearance; + } + } +} \ No newline at end of file diff --git a/S1API/Internal/Patches/ConsolePatches.cs b/S1API/Internal/Patches/ConsolePatches.cs index 78a56bb..f8c6bfa 100644 --- a/S1API/Internal/Patches/ConsolePatches.cs +++ b/S1API/Internal/Patches/ConsolePatches.cs @@ -4,11 +4,16 @@ using HarmonyLib; using S1API.Console; using S1API.Internal.Utils; +using UnityEngine; +using Object = UnityEngine.Object; #if (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using S1Console = ScheduleOne.Console; - +using S1CommandListScreen = ScheduleOne.CommandListScreen; +using TMPro; #elif (IL2CPPMELON) using S1Console = Il2CppScheduleOne.Console; +using S1CommandListScreen = Il2CppScheduleOne.CommandListScreen; +using Il2CppTMPro; #endif #if (IL2CPPMELON || IL2CPPBEPINEX) @@ -35,6 +40,7 @@ private static void AddCommands(S1Console __instance) if (__instance == null) return; + _addedCommandsToList.Clear(); var commandTypes = ReflectionUtils.GetDerivedClasses(); foreach (var type in commandTypes) { @@ -134,5 +140,64 @@ private static bool RouteCustomCommandsIl2Cpp(Il2CppSystem.Collections.Generic.L } } #endif + +#if (MONOMELON || MONOBEPINEX) + private static FieldInfo? _commandEntriesField; +#endif + + /// + /// Custom commands that were added to command list screen, stored here to prevent duplicate additions. + /// + private static HashSet _addedCommandsToList = new(); + + /// + /// Adds custom commands to command list screen. + /// + [HarmonyPatch(typeof(S1CommandListScreen), "Start")] + [HarmonyPostfix] + private static void AddCustomCommandEntries(S1CommandListScreen __instance) + { + try + { + if (__instance == null || __instance.CommandEntryPrefab == null || + __instance.CommandEntryContainer == null) + return; +#if (MONOMELON || MONOBEPINEX) + _commandEntriesField ??= + typeof(S1CommandListScreen) + .GetField("commandEntries", BindingFlags.NonPublic | BindingFlags.Instance); + var commandEntries = _commandEntriesField?.GetValue(__instance) as List; +#elif (IL2CPPMELON || IL2CPPBEPINEX) + var commandEntries = __instance?.commandEntries; +#endif + + foreach (var commandKey in CustomConsoleRegistry.registry.Keys) + { + try + { + if (_addedCommandsToList.Contains(commandKey)) + continue; + var rt = Object.Instantiate(__instance.CommandEntryPrefab, __instance.CommandEntryContainer); + rt.Find("Command").GetComponent().text = + CustomConsoleRegistry.registry[commandKey].CommandWord; + rt.Find("Description").GetComponent().text = + CustomConsoleRegistry.registry[commandKey].CommandDescription; + rt.Find("Example").GetComponent().text = + CustomConsoleRegistry.registry[commandKey].ExampleUsage; + + commandEntries?.Add(rt); + _addedCommandsToList.Add(commandKey); + } + catch (Exception e) + { + Logger.Warning($"[Console] Failed to add command '{commandKey}' to command list screen: {e.Message}"); + } + } + } + catch (Exception e) + { + Logger.Warning($"[Console] Failed to add custom commands to command list screen: {e.Message}"); + } + } } } \ No newline at end of file diff --git a/S1API/PhoneApp/PhoneApp.cs b/S1API/PhoneApp/PhoneApp.cs index fbd37f2..3536acf 100644 --- a/S1API/PhoneApp/PhoneApp.cs +++ b/S1API/PhoneApp/PhoneApp.cs @@ -648,6 +648,12 @@ private void Update() } } + private void OnDestroy() + { + // Destroy phone app when button handler is destroyed + phoneApp?.DestroyInternal(); + } + private bool IsHoveringButton() { // This is the same logic as native App.IsHoveringButton()