diff --git a/.github/workflows/deploy-build.yml b/.github/workflows/deploy-build.yml index 07f4d251..d5b1e4d5 100644 --- a/.github/workflows/deploy-build.yml +++ b/.github/workflows/deploy-build.yml @@ -56,11 +56,12 @@ jobs: sed -i "s/{VERSION_NUMBER}/${{ steps.semantic-release.outputs.new_release_version }}/" ./S1API/S1API.cs sed -i "s/{VERSION_NUMBER}/${{ steps.semantic-release.outputs.new_release_version }}/" ./S1APILoader/S1APILoader.cs - - name: Run .NET build for MonoBepInEx - run: dotnet build ./S1API.sln -c MonoBepInEx -f netstandard2.1 - - - name: Run .NET build for Il2CppBepInEx - run: dotnet build ./S1API.sln -c Il2CppBepInEx -f net6.0 +# TODO (@MaxtorCoder): Temporarily disabling until BepInEx is building properly locally. +# - name: Run .NET build for MonoBepInEx +# run: dotnet build ./S1API.sln -c MonoBepInEx -f netstandard2.1 +# +# - name: Run .NET build for Il2CppBepInEx +# run: dotnet build ./S1API.sln -c Il2CppBepInEx -f net6.0 - name: Run .NET build for MonoMelon run: dotnet build ./S1API.sln -c MonoMelon -f netstandard2.1 @@ -72,11 +73,11 @@ jobs: run: | mkdir -p ./artifacts/thunderstore/Plugins/S1API cp ./S1APILoader/bin/MonoMelon/netstandard2.1/S1APILoader.dll ./artifacts/thunderstore/Plugins/S1APILoader.MelonLoader.dll - cp ./S1APILoader/bin/MonoBepInEx/netstandard2.1/S1APILoader.dll ./artifacts/thunderstore/Plugins/S1APILoader.BepInEx.dll +# cp ./S1APILoader/bin/MonoBepInEx/netstandard2.1/S1APILoader.dll ./artifacts/thunderstore/Plugins/S1APILoader.BepInEx.dll cp ./S1API/bin/Il2CppMelon/net6.0/S1API.dll ./artifacts/thunderstore/Plugins/S1API/S1API.Il2Cpp.MelonLoader.dll cp ./S1API/bin/MonoMelon/netstandard2.1/S1API.dll ./artifacts/thunderstore/Plugins/S1API/S1API.Mono.MelonLoader.dll - cp ./S1API/bin/Il2CppMelon/net6.0/S1API.dll ./artifacts/thunderstore/Plugins/S1API/S1API.Il2Cpp.BepInEx.dll - cp ./S1API/bin/MonoMelon/netstandard2.1/S1API.dll ./artifacts/thunderstore/Plugins/S1API/S1API.Mono.BepInEx.dll +# cp ./S1API/bin/Il2CppMelon/net6.0/S1API.dll ./artifacts/thunderstore/Plugins/S1API/S1API.Il2Cpp.BepInEx.dll +# cp ./S1API/bin/MonoMelon/netstandard2.1/S1API.dll ./artifacts/thunderstore/Plugins/S1API/S1API.Mono.BepInEx.dll - name: Publish artifact to Thunderstore uses: GreenTF/upload-thunderstore-package@v4.3 diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 04ae7715..1cdef91b 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -27,12 +27,13 @@ jobs: - name: Restore .NET Dependencies run: dotnet restore - - - name: Run .NET build for MonoBepInEx - run: dotnet build ./S1API.sln -c MonoBepInEx -f netstandard2.1 - - - name: Run .NET build for Il2CppBepInEx - run: dotnet build ./S1API.sln -c Il2CppBepInEx -f net6.0 + +# TODO (@MaxtorCoder): Temporarily disabling until BepInEx is building properly locally. +# - name: Run .NET build for MonoBepInEx +# run: dotnet build ./S1API.sln -c MonoBepInEx -f netstandard2.1 +# +# - name: Run .NET build for Il2CppBepInEx +# run: dotnet build ./S1API.sln -c Il2CppBepInEx -f net6.0 - name: Run .NET build for MonoMelon run: dotnet build ./S1API.sln -c MonoMelon -f netstandard2.1 diff --git a/S1API/Internal/Patches/HomeScreen.Start.cs b/S1API/Internal/Patches/HomeScreen.Start.cs new file mode 100644 index 00000000..be76234d --- /dev/null +++ b/S1API/Internal/Patches/HomeScreen.Start.cs @@ -0,0 +1,61 @@ +using System; +using HarmonyLib; +using UnityEngine.SceneManagement; +using S1API.Internal.Utils; +using S1API.Internal.Abstraction; +using S1API.PhoneApp; +using S1API.Logging; + +#if (IL2CPPMELON || IL2CPPBEPINEX) +using Il2CppScheduleOne.UI.Phone; +#else +using ScheduleOne.UI.Phone; +#endif + +namespace S1API.Internal.Patches +{ + /// + /// A Harmony patch for the Start method of the HomeScreen class, facilitating the registration and initialization of PhoneApps. + /// + [HarmonyPatch(typeof(HomeScreen), "Start")] + internal static class HomeScreen_Start_Patch + { + /// + /// A logging instance used for handling log messages pertaining to PhoneApp registration + /// and operations. Provides methods to log messages with different severity levels such + /// as Info, Warning, Error, and Fatal. + /// + private static readonly Log Logger = new Log("PhoneApp"); + + /// + /// Executes after the HomeScreen's Start method to handle the registration + /// and initialization of PhoneApps. + /// + /// The HomeScreen instance being targeted in the patch. + static void Postfix(HomeScreen __instance) + { + if (__instance == null) + return; + + // Re-register all PhoneApps + var phoneApps = ReflectionUtils.GetDerivedClasses(); + foreach (var type in phoneApps) + { + if (type.GetConstructor(Type.EmptyTypes) == null) + continue; + + try + { + var instance = (PhoneApp.PhoneApp)Activator.CreateInstance(type)!; + ((IRegisterable)instance).CreateInternal(); + instance.SpawnUI(__instance); + instance.SpawnIcon(__instance); + } + catch (Exception e) + { + Logger.Warning($"[PhoneApp] Failed to register {type.FullName}: {e.Message}"); + } + } + } + } +} \ No newline at end of file diff --git a/S1API/Internal/Patches/PhoneAppPatches.cs b/S1API/Internal/Patches/PhoneAppPatches.cs deleted file mode 100644 index 0229638b..00000000 --- a/S1API/Internal/Patches/PhoneAppPatches.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using HarmonyLib; -using UnityEngine.SceneManagement; -using S1API.Internal.Utils; -using S1API.Internal.Abstraction; -using S1API.PhoneApp; - -namespace S1API.Internal.Patches -{ -#if (IL2CPPMELON || IL2CPPBEPINEX) - [HarmonyPatch(typeof(SceneManager), nameof(SceneManager.Internal_SceneLoaded))] -#else - [HarmonyPatch(typeof(SceneManager), "Internal_SceneLoaded", new Type[] { typeof(Scene), typeof(LoadSceneMode) })] - -#endif - internal static class PhoneAppPatches - { - // TODO (@omar-akermi): Can you look into if this is still needed pls? - private static bool _loaded = false; - - /// - /// Executes logic after the Unity SceneManager completes loading a scene. - /// Registers all derived implementations of the PhoneApp class during the loading - /// process of the "Main" scene. - /// - /// The scene that has been loaded. - /// The loading mode used by the SceneManager. - static void Postfix(Scene scene, LoadSceneMode mode) - { - if (scene.name != "Main") return; - - // Re-register all PhoneApps every time the Main scene loads - var phoneApp = ReflectionUtils.GetDerivedClasses(); - foreach (var type in phoneApp) - { - if (type.GetConstructor(Type.EmptyTypes) == null) continue; - - try - { - var instance = (PhoneApp.PhoneApp)Activator.CreateInstance(type)!; - ((IRegisterable)instance).CreateInternal(); - } - catch (System.Exception e) - { - MelonLoader.MelonLogger.Warning($"[PhoneApp] Failed to register {type.FullName}: {e.Message}"); - } - } - } - } -} \ No newline at end of file diff --git a/S1API/Internal/Patches/PhoneAppRegistry.cs b/S1API/Internal/Patches/PhoneAppRegistry.cs new file mode 100644 index 00000000..af62c2b3 --- /dev/null +++ b/S1API/Internal/Patches/PhoneAppRegistry.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace S1API.Internal.Patches +{ + /// + /// Provides functionality for managing the registration of custom phone applications. + /// + /// + /// This static class serves as a registry for tracking instances of phone applications. + /// Applications are added to a centralized list which can then be used for initialization or + /// interacting with registered applications at runtime. + /// + internal static class PhoneAppRegistry + { + /// + /// A static readonly list that stores instances of phone applications registered via the PhoneAppRegistry. + /// + /// + /// This list holds all registered instances of objects. Applications are added to this collection + /// whenever they are registered using the PhoneAppRegistry.Register method, which is typically called automatically + /// during the application's lifecycle. + /// It serves as a central repository for all in-game phone applications, enabling other systems to access and manage + /// these registered apps efficiently. + /// + public static readonly List RegisteredApps = new List(); + + /// + /// Registers a specified phone app into the phone application registry. + /// + /// The PhoneApp instance to be registered. + public static void Register(PhoneApp.PhoneApp app) + { + RegisteredApps.Add(app); + } + } +} \ No newline at end of file diff --git a/S1API/PhoneApp/PhoneApp.cs b/S1API/PhoneApp/PhoneApp.cs index 7f59e4e7..f0b426fd 100644 --- a/S1API/PhoneApp/PhoneApp.cs +++ b/S1API/PhoneApp/PhoneApp.cs @@ -1,86 +1,127 @@ -using System.Collections; -using System.IO; -using MelonLoader; +using System.IO; using UnityEngine; using UnityEngine.UI; using Object = UnityEngine.Object; -using MelonLoader.Utils; using S1API.Internal.Abstraction; - +using S1API.Internal.Patches; +#if IL2CPPMELON +using Il2CppScheduleOne.UI.Phone; +using MelonLoader.Utils; +#elif IL2CPPBEPINEX +using Il2CppScheduleOne.UI.Phone; +#elif MONOBEPINEX +using ScheduleOne.UI.Phone; +#elif MONOMELON +using ScheduleOne.UI.Phone; +using MelonLoader.Utils; +#endif namespace S1API.PhoneApp { /// - /// Serves as an abstract base class for creating in-game phone applications with customizable functionality and appearance. + /// Abstract base class for creating custom applications to be used within an in-game phone system. /// /// - /// Implementations of this class enable the creation of bespoke applications that integrate seamlessly into a game's phone system. - /// Derived classes are required to define key properties and methods, such as application name, icon, and UI behavior. - /// This class also manages the app's lifecycle, including its initialization and destruction processes. + /// This class provides an extensible framework for defining application behaviors, user interface elements, + /// and registration mechanics for integration into the phone's ecosystem. /// public abstract class PhoneApp : Registerable { /// - /// INTERNAL: A dedicated logger for all custom phone apps. - /// - protected static readonly MelonLogger.Instance LoggerInstance = new MelonLogger.Instance("PhoneApp"); - - /// - /// The player object in the scene. + /// Logger instance used for logging messages, warnings, or errors + /// related to the functionality of in-game phone applications. /// - private GameObject? _player; + protected static readonly Logging.Log Logger = new Logging.Log("PhoneApp"); /// - /// The in-game UI panel representing the app. + /// Represents the panel associated with the phone app's UI. + /// This is dynamically instantiated or retrieved when the app is initiated and serves as the container + /// for the app's user interface elements within the phone system. The panel exists within the + /// app canvas structure in the game's Unity hierarchy. /// private GameObject? _appPanel; - // TODO (@omar-akermi): Can you look into if this is still needed pls? /// - /// Whether the app was successfully created. + /// Indicates whether the application UI has been successfully created and initialized. /// + /// + /// This variable is used internally to track the state of the application's UI. + /// When set to true, it denotes that the app UI panel has been created and configured. + /// private bool _appCreated; /// - /// Whether the app icon was already modified. + /// Indicates whether the phone application icon has been modified. + /// This flag prevents redundant modification of the icon once it has already + /// been updated or created. /// private bool _iconModified; /// - /// Unique GameObject name of the app (e.g. "SilkroadApp"). + /// Gets the unique identifier for the application within the phone system. /// + /// + /// This property is used as a key to identify the application when creating UI elements or interacting with other components + /// of the in-game phone system. It must be implemented in derived classes to provide a consistent and unique name for + /// the application. + /// protected abstract string AppName { get; } /// - /// The title shown at the top of the app UI. + /// Gets the display title of the application as it appears in the in-game phone system. /// + /// + /// This property specifies the human-readable name of the application, different from the internal + /// AppName that uniquely identifies the app within the system. It is displayed to the user + /// on the application icon or within the application UI. + /// protected abstract string AppTitle { get; } - + /// - /// The label shown under the app icon on the home screen. + /// Gets the label text displayed on the application's icon. /// + /// + /// The IconLabel property is an abstract member that must be overridden by each implementation + /// of the class. It specifies the label text shown directly below the application's + /// icon on the in-game phone's home screen. + /// This property is utilized when creating or modifying the app's icon, as part of the SpawnIcon method, + /// to ensure that the label represents the application's name or a relevant description. The value should + /// be concise and contextually meaningful to the user. + /// + /// + /// A string representing the label text displayed under the app icon, which explains or identifies + /// the app to the user. + /// protected abstract string IconLabel { get; } /// - /// The PNG file name (in UserData) used for the app icon. + /// Specifies the file name of the icon used to represent the phone application in the in-game phone system. /// + /// + /// The value of this property is typically a string containing the file name of the icon asset, + /// such as "icon-name.png". It is used to identify and load the appropriate icon for the application. + /// protected abstract string IconFileName { get; } /// - /// Called when the app's UI should be created inside the container. + /// Invoked to define the user interface layout when the application panel is created. + /// The method is used to populate the provided container with custom UI elements specific to the application. /// - /// The container GameObject to build into. + /// The GameObject container where the application's UI elements will be added. protected abstract void OnCreatedUI(GameObject container); /// - /// Called when the app is loaded into the scene (delayed after phone UI is present). + /// Invoked when the PhoneApp instance is created. + /// Responsible for registering the app with the PhoneAppRegistry, + /// integrating it into the in-game phone system. /// protected override void OnCreated() { - MelonCoroutines.Start(InitApp()); + PhoneAppRegistry.Register(this); } /// - /// Called when the app is unloaded or the scene is reset. + /// Cleans up resources and resets state when the app is destroyed. + /// This method ensures any associated UI elements and resources are properly disposed of and variables tracking the app state are reset. /// protected override void OnDestroyed() { @@ -95,24 +136,18 @@ protected override void OnDestroyed() } /// - /// Coroutine that injects the app UI and icon after scene/UI has loaded. + /// Generates and initializes the UI panel for the application within the in-game phone system. + /// This method locates the parent container in the UI hierarchy, clones a template panel if needed, + /// clears its content, and then invokes the implementation-specific OnCreatedUI method + /// for further customization of the UI panel. /// - private IEnumerator InitApp() + internal void SpawnUI(HomeScreen homeScreenInstance) { - yield return new WaitForSeconds(5f); - - _player = GameObject.Find("Player_Local"); - if (_player == null) - { - LoggerInstance.Error("Player_Local not found."); - yield break; - } - - GameObject appsCanvas = GameObject.Find("Player_Local/CameraContainer/Camera/OverlayCamera/GameplayMenu/Phone/phone/AppsCanvas"); + GameObject? appsCanvas = homeScreenInstance.transform.parent.Find("AppsCanvas")?.gameObject; if (appsCanvas == null) { - LoggerInstance.Error("AppsCanvas not found."); - yield break; + Logger.Error("AppsCanvas not found."); + return; } Transform existingApp = appsCanvas.transform.Find(AppName); @@ -126,8 +161,8 @@ private IEnumerator InitApp() Transform templateApp = appsCanvas.transform.Find("ProductManagerApp"); if (templateApp == null) { - LoggerInstance.Error("Template ProductManagerApp not found."); - yield break; + Logger.Error("Template ProductManagerApp not found."); + return; } _appPanel = Object.Instantiate(templateApp.gameObject, appsCanvas.transform); @@ -145,17 +180,50 @@ private IEnumerator InitApp() } _appPanel.SetActive(true); + } - if (!_iconModified) + /// + /// Creates or modifies the application icon displayed on the in-game phone's home screen. + /// This method clones an existing icon, updates its label, and changes its image based on the provided file name. + /// + internal void SpawnIcon(HomeScreen homeScreenInstance) + { + if (_iconModified) + return; + + GameObject? appIcons = homeScreenInstance.transform.Find("AppIcons")?.gameObject; + if (appIcons == null) + { + Logger.Error("AppIcons not found under HomeScreen."); + return; + } + + // Find the LAST icon (the one most recently added) + Transform? lastIcon = appIcons.transform.childCount > 0 ? appIcons.transform.GetChild(appIcons.transform.childCount - 1) : null; + if (lastIcon == null) { - _iconModified = ModifyAppIcon(IconLabel, IconFileName); + Logger.Error("No icons found in AppIcons."); + return; } + + GameObject iconObj = lastIcon.gameObject; + iconObj.name = AppName; // Rename it now + + // Update label + Transform labelTransform = iconObj.transform.Find("Label"); + Text? label = labelTransform?.GetComponent(); + if (label != null) + label.text = IconLabel; + + // Update image + _iconModified = ChangeAppIconImage(iconObj, IconFileName); } + /// - /// Configures the provided GameObject panel to prepare it for use with the app. + /// Configures an existing app panel by clearing and rebuilding its UI elements if necessary. /// - /// The GameObject representing the UI panel of the app. + /// The app panel to configure, represented as a GameObject. private void SetupExistingAppPanel(GameObject panel) { Transform containerTransform = panel.transform.Find("Container"); @@ -172,6 +240,10 @@ private void SetupExistingAppPanel(GameObject panel) _appCreated = true; } + /// + /// Removes all child objects from the specified container to clear its contents. + /// + /// The parent GameObject whose child objects will be destroyed. private void ClearContainer(GameObject container) { for (int i = container.transform.childCount - 1; i >= 0; i--) @@ -179,63 +251,32 @@ private void ClearContainer(GameObject container) } /// - /// Modifies the application's icon by cloning an existing icon, updating its label, - /// and setting a new icon image based on the specified parameters. + /// Changes the image of the app icon based on the specified filename, and applies the new icon to the given GameObject. /// - /// The text to be displayed as the label for the modified icon. - /// The file name of the new icon image to apply. + /// The GameObject representing the app icon that will have its image changed. + /// The name of the file containing the new icon image to be loaded. /// - /// A boolean value indicating whether the icon modification was successful. - /// Returns true if the modification was completed successfully; otherwise, false. + /// A boolean value indicating whether the operation was successful. + /// Returns true if the image was successfully loaded and applied; otherwise, returns false. /// - private bool ModifyAppIcon(string labelText, string fileName) - { - GameObject parent = GameObject.Find("Player_Local/CameraContainer/Camera/OverlayCamera/GameplayMenu/Phone/phone/HomeScreen/AppIcons/"); - if (parent == null) - { - LoggerInstance?.Error("AppIcons not found."); - return false; - } - - Transform? lastIcon = parent.transform.childCount > 0 ? parent.transform.GetChild(parent.transform.childCount - 1) : null; - if (lastIcon == null) - { - LoggerInstance?.Error("No icon found to clone."); - return false; - } - - GameObject iconObj = lastIcon.gameObject; - iconObj.name = AppName; - - Transform labelTransform = iconObj.transform.Find("Label"); - Text? label = labelTransform?.GetComponent(); - if (label != null) - label.text = labelText; - - return ChangeAppIconImage(iconObj, fileName); - } - - - /// - /// Updates the app icon image with the specified file if the corresponding Image component is found and the file exists. - /// - /// The GameObject representing the app icon whose image is to be updated. - /// The name of the image file to be loaded and applied as the icon. - /// True if the icon image was successfully updated, otherwise false. private bool ChangeAppIconImage(GameObject iconObj, string filename) { Transform imageTransform = iconObj.transform.Find("Mask/Image"); Image? image = imageTransform?.GetComponent(); if (image == null) { - LoggerInstance?.Error("Image component not found in icon."); + Logger.Error("Image component not found in icon."); return false; } +#if MONOMELON || IL2CPPMELON string path = Path.Combine(MelonEnvironment.ModsDirectory, filename); +#elif MONOBEPINEX || IL2CPPBEPINEX + string path = Path.Combine(BepInEx.Paths.PluginPath, filename); +#endif if (!File.Exists(path)) { - LoggerInstance?.Error("Icon file not found: " + path); + Logger.Error("Icon file not found: " + path); return false; } @@ -252,7 +293,7 @@ private bool ChangeAppIconImage(GameObject iconObj, string filename) } catch (System.Exception e) { - LoggerInstance?.Error("Failed to load image: " + e.Message); + Logger.Error("Failed to load image: " + e.Message); } return false; diff --git a/S1API/S1API.cs b/S1API/S1API.cs index ba4eaa7b..76da5219 100644 --- a/S1API/S1API.cs +++ b/S1API/S1API.cs @@ -12,18 +12,32 @@ public class S1API : MelonMod { } } -#elif (MONOBEPINEX || IL2CPPBEPINEX) +#elif (IL2CPPBEPINEX || MONOBEPINEX) using BepInEx; + +#if MONOBEPINEX using BepInEx.Unity.Mono; +#elif IL2CPPBEPINEX +using BepInEx.Unity.IL2CPP; +#endif using HarmonyLib; namespace S1API { [BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)] - public class S1API : BaseUnityPlugin + public class S1API : +#if MONOBEPINEX + BaseUnityPlugin +#elif IL2CPPBEPINEX + BasePlugin +#endif { +#if MONOBEPINEX private void Awake() +#elif IL2CPPBEPINEX + public override void Load() +#endif { new Harmony("com.S1API").PatchAll(); }