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();
}