-
-
Notifications
You must be signed in to change notification settings - Fork 22
Issues backlog: PhoneApp onDestroyed, custom commands enhancement, employee appearances, more checks in CombatBehaviour #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: stable
Are you sure you want to change the base?
Changes from all commits
01b05f1
d2563ce
a8c0dfa
b6197c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| { | ||
| /// <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; | ||
| } | ||
|
|
||
| return new EmployeeAppearance(S1Employees.EmployeeManager.Instance.GetAppearance(male, index)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find all declarations/usages to confirm return types and failure conventions.
rg -nP --type=cs -C3 '\b(GetAppearance|GetRandomAppearance)\s*\('
rg -nP --type=cs -C3 '\bclass\s+EmployeeAppearance\b|\bclass\s+AvatarSettings\b'Repository: ifBars/S1API Length of output: 3944 Fix null-safety at wrapper boundary in
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /// <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); | ||
| 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; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reset duplicate tracking when the command list screen starts.
Suggested change- _addedCommandsToList.Clear();
var commandTypes = ReflectionUtils.GetDerivedClasses<BaseConsoleCommand>();
foreach (var type in commandTypes)
{
Logger.Msg($"Found console command: {type.FullName}");
@@
private static void AddCustomCommandEntries(S1CommandListScreen __instance)
{
try
{
+ _addedCommandsToList.Clear();
if (__instance == null || __instance.CommandEntryPrefab == null ||
__instance.CommandEntryContainer == null)
return;Also applies to: 151-164 🤖 Prompt for AI Agents |
||
| var commandTypes = ReflectionUtils.GetDerivedClasses<BaseConsoleCommand>(); | ||
| 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 | ||
|
|
||
| /// <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; | ||
| #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 commandKey in CustomConsoleRegistry.registry.Keys) | ||
| { | ||
| try | ||
| { | ||
| if (_addedCommandsToList.Contains(commandKey)) | ||
| continue; | ||
| var rt = Object.Instantiate(__instance.CommandEntryPrefab, __instance.CommandEntryContainer); | ||
| rt.Find("Command").GetComponent<TextMeshProUGUI>().text = | ||
| CustomConsoleRegistry.registry[commandKey].CommandWord; | ||
| rt.Find("Description").GetComponent<TextMeshProUGUI>().text = | ||
| CustomConsoleRegistry.registry[commandKey].CommandDescription; | ||
| rt.Find("Example").GetComponent<TextMeshProUGUI>().text = | ||
| CustomConsoleRegistry.registry[commandKey].ExampleUsage; | ||
|
|
||
| commandEntries?.Add(rt); | ||
| _addedCommandsToList.Add(commandKey); | ||
|
Comment on lines
+174
to
+189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't inject rows for commands the base console will shadow. Both routing prefixes hand execution back to 🤖 Prompt for AI Agents |
||
| } | ||
| 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}"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -648,6 +648,12 @@ private void Update() | |
| } | ||
| } | ||
|
|
||
| private void OnDestroy() | ||
| { | ||
| // Destroy phone app when button handler is destroyed | ||
| phoneApp?.DestroyInternal(); | ||
| } | ||
|
Comment on lines
+651
to
+655
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent re-entrant destruction from handler teardown.
Suggested guard-based fix public abstract class PhoneApp : Registerable
{
+ private bool _isDestroying;
+ internal bool IsDestroying => _isDestroying;
...
protected override void OnDestroyed()
{
+ if (_isDestroying) return;
+ _isDestroying = true;
+
if (_appPanel != null)
{
Object.Destroy(_appPanel);
_appPanel = null;
}
...
}
}
...
internal class PhoneAppButtonHandler : MonoBehaviour
{
internal PhoneApp phoneApp;
...
private void OnDestroy()
{
- // Destroy phone app when button handler is destroyed
- phoneApp?.DestroyInternal();
+ if (phoneApp != null && !phoneApp.IsDestroying)
+ phoneApp.DestroyInternal();
}
}🤖 Prompt for AI Agents |
||
|
|
||
| private bool IsHoveringButton() | ||
| { | ||
| // This is the same logic as native App<T>.IsHoveringButton() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In Unity, does Object.Instantiate(GameObject) create a runtime scene clone that should be destroyed if used only for metadata/type checks?💡 Result:
Yes, calling Object.Instantiate(GameObject) creates a complete, functional clone of the object hierarchy in the runtime scene [1][2]. It is an expensive operation that performs serialization and deserialization, allocates memory, registers the new objects with the engine's internal systems (physics, rendering, scripting), and triggers lifecycle methods like Awake and OnEnable [1][3]. Using Instantiate solely for metadata or type checks is strongly discouraged because: 1. Performance Impact: It incurs significant CPU and memory overhead, including Garbage Collection (GC) allocations, which can cause performance hitches [4][3]. 2. Lifecycle Side Effects: Because it creates a full clone, components' Awake and OnEnable methods will execute on the new instance, which may cause unintended side effects or errors if your code is not designed for it [1]. 3. Resource Management: If you instantiate an object for a temporary check, you must manually call Object.Destroy(clone) to remove it from the scene; otherwise, you will create a memory leak and clutter your scene with useless objects [3]. For metadata or type checks, you should access the original GameObject or prefab asset directly without instantiating it. You can access components, check types (e.g., using GetComponent or Type comparisons), and read serializable data directly from the reference without creating a runtime clone [1][2]. If you need to inspect an asset's properties that are only available at runtime, consider using Editor-time tools or custom serialization approaches that do not require full instantiation [2][5].
Citations:
🏁 Script executed:
Repository: ifBars/S1API
Length of output: 148
🏁 Script executed:
Repository: ifBars/S1API
Length of output: 3033
🏁 Script executed:
Repository: ifBars/S1API
Length of output: 3271
🏁 Script executed:
Repository: ifBars/S1API
Length of output: 3627
🏁 Script executed:
Repository: ifBars/S1API
Length of output: 11307
🏁 Script executed:
Repository: ifBars/S1API
Length of output: 11934
Avoid
InstantiateinDefaultWeaponAssetPathsetter (lines 92-93)Object.Instantiate(gameObject)creates a full runtime clone every timeDefaultWeaponAssetPathis set, and this code path never destroys that instance. Instead, validate the prefab asset directly viaGetComponent/GetComponentInChildrento avoid extra allocations and lifecycle side effects.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents