Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions S1API/Console/CustomConsoleRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ internal static class CustomConsoleRegistry

private static readonly Dictionary<string, BaseConsoleCommand> registry = new Dictionary<string, BaseConsoleCommand>(StringComparer.OrdinalIgnoreCase);

internal static IReadOnlyDictionary<string, BaseConsoleCommand> RegisteredCommands => registry;

internal static void Register(BaseConsoleCommand command)
{
if (command == null)
Expand Down
31 changes: 23 additions & 8 deletions S1API/Entities/Behaviour/CombatBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using UnityEngine;
using MelonLoader;
using S1API.Entities.Equippables;
using S1API.Internal.Utils;
using Object = UnityEngine.Object;

namespace S1API.Entities.Behaviour;
Expand All @@ -22,6 +23,8 @@ namespace S1API.Entities.Behaviour;
/// </summary>
public class CombatBehaviour
{
private static readonly Logging.Log Logger = new("CombatBehaviour");

/// <summary>
/// INTERNAL: NPC reference
/// </summary>
Expand Down Expand Up @@ -57,8 +60,13 @@ public float GiveUpTime
/// <summary>
/// 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.
/// <see cref="Weapon"/> for convenience when setting this property.
/// Use <see cref="Weapon"/> or <see cref="AvatarEquippablePaths"/> for convenience when setting this property.
/// Set to an empty string to clear the default weapon.
/// </summary>
/// <remarks>
/// Using type-safe <see cref="SetDefaultWeapon(Equippable)"/>
/// or <see cref="SetDefaultWeapon(EquippableBuilder)"/> is generally preferred and advised.
/// </remarks>
public string DefaultWeaponAssetPath
{
get
Expand All @@ -74,21 +82,28 @@ public string DefaultWeaponAssetPath
return;
}

var go = Resources.Load(value) as GameObject;
if (go == null)
var go = Resources.Load(value);
if (!CrossType.Is<GameObject>(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<AvatarEquippable>();
var equippable = gameObject.GetComponent<AvatarEquippable>() ??
gameObject.GetComponentInChildren<AvatarEquippable>(true);
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<AvatarWeapon>(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;
}
}

Expand Down Expand Up @@ -256,4 +271,4 @@ private string GetAssetPathFromEquippable(Equippable equippable)

return avatarEquippable?.AssetPath;
}
}
}
96 changes: 96 additions & 0 deletions S1API/Entities/Employees/EmployeeManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#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
{
/// <summary>
/// Provides methods for managing employee appearances and related data.
/// </summary>
public static class EmployeeManager
{
private static readonly Logging.Log Logger = new("EmployeeManager");

/// <summary>
/// Gets an employee appearance by index.
/// </summary>
/// <param name="male">Whether to choose from male employee appearance pool</param>
/// <param name="index">The index of the appearance to retrieve</param>
/// <returns>An <see cref="EmployeeAppearance"/> representing the employee appearance at the specified index if successful; otherwise, null.</returns>
public static EmployeeAppearance? GetAppearance(bool male, int index)
{
if (!S1Employees.EmployeeManager.InstanceExists)
{
Logger.Error("EmployeeManager instance does not exist; cannot get appearance");
return null;
}

var appearance = S1Employees.EmployeeManager.Instance.GetAppearance(male, index);
return appearance == null ? null : new EmployeeAppearance(appearance);
}

/// <summary>
/// Gets a random employee appearance.
/// </summary>
/// <param name="male">Whether to choose from male employee appearance pool</param>
/// <param name="index">The index of the appearance that was retrieved</param>
/// <param name="settings">The avatar settings of the appearance that was retrieved</param>
/// <returns>True if an appearance was successfully retrieved; otherwise, false.</returns>
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);
if (avatarSettings == null)
{
settings = null;
index = -1;
return false;
}

index = i;
settings = new AvatarSettings(avatarSettings);
return true;
}
}

/// <summary>
/// Represents an employee appearance, including avatar settings and mugshot sprite.
/// </summary>
public class EmployeeAppearance
{
/// <summary>
/// INTERNAL: The underlying employee appearance from the base game.
/// </summary>
internal S1Employees.EmployeeManager.EmployeeAppearance S1EmployeeAppearance;

/// <summary>
/// Gets the avatar settings associated with this employee appearance.
/// </summary>
public AvatarSettings Settings => new(S1EmployeeAppearance.Settings);

/// <summary>
/// Gets the mugshot sprite associated with this employee appearance.
/// </summary>
public Sprite Mugshot => S1EmployeeAppearance.Mugshot;

/// <summary>
/// INTERNAL: Initializes a new instance of the EmployeeAppearance class with the specified base game type appearance.
/// </summary>
internal EmployeeAppearance(S1Employees.EmployeeManager.EmployeeAppearance s1EmployeeAppearance)
{
S1EmployeeAppearance = s1EmployeeAppearance;
}
}
}
91 changes: 89 additions & 2 deletions S1API/Internal/Patches/ConsolePatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -134,5 +139,87 @@ private static bool RouteCustomCommandsIl2Cpp(Il2CppSystem.Collections.Generic.L
}
}
#endif

#if (MONOMELON || MONOBEPINEX)
private static FieldInfo? _commandEntriesField;
#endif

/// <summary>
/// Custom commands that were added to command list screen, stored here to prevent duplicate additions.
/// </summary>
private static HashSet<string> _addedCommandsToList = new();

/// <summary>
/// Adds custom commands to command list screen.
/// </summary>
[HarmonyPatch(typeof(S1CommandListScreen), "Start")]
[HarmonyPostfix]
private static void AddCustomCommandEntries(S1CommandListScreen __instance)
{
try
{
if (__instance == null || __instance.CommandEntryPrefab == null ||
__instance.CommandEntryContainer == null)
return;

_addedCommandsToList.Clear();
#if (MONOMELON || MONOBEPINEX)
_commandEntriesField ??=
typeof(S1CommandListScreen)
.GetField("commandEntries", BindingFlags.NonPublic | BindingFlags.Instance);
var commandEntries = _commandEntriesField?.GetValue(__instance) as List<RectTransform>;
#elif (IL2CPPMELON || IL2CPPBEPINEX)
var commandEntries = __instance?.commandEntries;
#endif

foreach (var command in CustomConsoleRegistry.RegisteredCommands)
{
var commandKey = command.Key;
try
{
if (_addedCommandsToList.Contains(commandKey))
continue;
if (IsNativeCommand(commandKey))
continue;

var rt = Object.Instantiate(__instance.CommandEntryPrefab, __instance.CommandEntryContainer);
rt.Find("Command").GetComponent<TextMeshProUGUI>().text =
command.Value.CommandWord;
rt.Find("Description").GetComponent<TextMeshProUGUI>().text =
command.Value.CommandDescription;
rt.Find("Example").GetComponent<TextMeshProUGUI>().text =
command.Value.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}");
}
}

private static bool IsNativeCommand(string commandKey)
{
if (string.IsNullOrWhiteSpace(commandKey))
return false;

#if (MONOMELON || MONOBEPINEX)
_monoCommandsField ??= typeof(S1Console).GetField("commands", BindingFlags.NonPublic | BindingFlags.Static);
var dict = _monoCommandsField?.GetValue(null) as IDictionary<string, S1Console.ConsoleCommand>;
return dict != null && dict.ContainsKey(commandKey);
#elif (IL2CPPMELON || IL2CPPBEPINEX)
var dict = S1Console.commands;
return dict != null && dict.ContainsKey(commandKey);
#else
return false;
#endif
}
}
}
}
17 changes: 16 additions & 1 deletion S1API/PhoneApp/PhoneApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ public abstract class PhoneApp : Registerable
/// app canvas structure in the game's Unity hierarchy.
/// </summary>
private GameObject? _appPanel;
private bool _isDestroying;

internal bool IsDestroying => _isDestroying;

/// <summary>
/// Indicates whether the application UI has been successfully created and initialized.
Expand Down Expand Up @@ -187,6 +190,11 @@ protected override void OnCreated()
/// </summary>
protected override void OnDestroyed()
{
if (_isDestroying)
return;

_isDestroying = true;

if (_appPanel != null)
{
Object.Destroy(_appPanel);
Expand Down Expand Up @@ -648,6 +656,13 @@ private void Update()
}
}

private void OnDestroy()
{
// Destroy phone app when button handler is destroyed
if (phoneApp != null && !phoneApp.IsDestroying)
phoneApp.DestroyInternal();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private bool IsHoveringButton()
{
// This is the same logic as native App<T>.IsHoveringButton()
Expand All @@ -658,4 +673,4 @@ private bool IsHoveringButton()
return false;
}
}
}
}
Loading