From c46dcbb06c243317f9e9afdd7de0d7022129be77 Mon Sep 17 00:00:00 2001 From: Omar Akermi Date: Sun, 20 Apr 2025 01:08:53 +0200 Subject: [PATCH 1/3] PhoneApp --- S1API/PhoneApp/MyAwesomeApp.cs | 20 +++ S1API/PhoneApp/PhoneApp.cs | 249 ++++++++++++++++++++++++++++++ S1API/PhoneApp/PhoneAppManager.cs | 46 ++++++ S1API/PhoneApp/UIFactory.cs | 138 +++++++++++++++++ S1API/PhoneApp/readme.md | 63 ++++++++ S1API/S1API.cs | 20 ++- 6 files changed, 534 insertions(+), 2 deletions(-) create mode 100644 S1API/PhoneApp/MyAwesomeApp.cs create mode 100644 S1API/PhoneApp/PhoneApp.cs create mode 100644 S1API/PhoneApp/PhoneAppManager.cs create mode 100644 S1API/PhoneApp/UIFactory.cs create mode 100644 S1API/PhoneApp/readme.md diff --git a/S1API/PhoneApp/MyAwesomeApp.cs b/S1API/PhoneApp/MyAwesomeApp.cs new file mode 100644 index 00000000..2a5fa577 --- /dev/null +++ b/S1API/PhoneApp/MyAwesomeApp.cs @@ -0,0 +1,20 @@ +using UnityEngine; +using UnityEngine.UI; +using S1API.PhoneApp; + +namespace S1API.PhoneApp +{ + public class MyAwesomeApp : PhoneApp + { + protected override string AppName => "MyAwesomeApp"; + protected override string AppTitle => "My Awesome App"; + protected override string IconLabel => "Awesome"; + protected override string IconFileName => "my_icon.png"; + + protected override void OnCreated(GameObject container) + { + GameObject panel = UIFactory.Panel("MainPanel", container.transform, Color.black); + UIFactory.Text("HelloText", "Hello from My Awesome App!", panel.transform, 22, TextAnchor.MiddleCenter, FontStyle.Bold); + } + } +} diff --git a/S1API/PhoneApp/PhoneApp.cs b/S1API/PhoneApp/PhoneApp.cs new file mode 100644 index 00000000..53f6136d --- /dev/null +++ b/S1API/PhoneApp/PhoneApp.cs @@ -0,0 +1,249 @@ +#if IL2CPP +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.Events; +#elif MONO +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.Events; +#endif + +using System.Collections; +using System.IO; +using MelonLoader; +using Object = UnityEngine.Object; +using MelonLoader.Utils; + +namespace S1API.PhoneApp +{ + /// + /// Base class for defining in-game phone apps. Automatically clones the phone UI, + /// injects your app, and supports icon customization. + /// + public abstract class PhoneApp + { + /// + /// Reference to the player object in the scene. + /// + protected GameObject Player; + + /// + /// The actual app panel instance in the phone UI. + /// + protected GameObject AppPanel; + + /// + /// Whether the app panel was created by this instance. + /// + protected bool AppCreated; + + /// + /// Tracks whether the app icon has been injected into the home screen. + /// + protected bool IconModified; + + /// + /// Prevents double-initialization. + /// + protected bool InitializationStarted; + + /// + /// Unique internal name for the app GameObject. + /// + protected abstract string AppName { get; } + + /// + /// Title text displayed at the top of the app UI. + /// + protected abstract string AppTitle { get; } + + /// + /// Label shown below the app icon on the phone home screen. + /// + protected abstract string IconLabel { get; } + + /// + /// PNG filename for the app icon (must be placed in UserData folder). + /// + protected abstract string IconFileName { get; } + + /// + /// Called after the app is created and a UI container is available. + /// Implement your custom UI here. + /// + /// The GameObject container inside the app panel. + protected abstract void OnCreated(GameObject container); + + /// + /// Begins async setup of the app, including icon and panel creation. + /// Should only be called once per session. + /// + /// Logger to report errors and status. + public void Init(MelonLogger.Instance logger) + { + if (!InitializationStarted) + { + InitializationStarted = true; + MelonCoroutines.Start(DelayedInit(logger)); + } + } + + /// + /// Coroutine that delays setup to ensure all UI elements are ready. + /// + private IEnumerator DelayedInit(MelonLogger.Instance logger) + { + yield return new WaitForSeconds(5f); + + Player = GameObject.Find("Player_Local"); + if (Player == null) + { + logger.Error("Player_Local not found."); + yield break; + } + + GameObject appsCanvas = GameObject.Find("Player_Local/CameraContainer/Camera/OverlayCamera/GameplayMenu/Phone/phone/AppsCanvas"); + if (appsCanvas == null) + { + logger.Error("AppsCanvas not found."); + yield break; + } + + Transform existingApp = appsCanvas.transform.Find(AppName); + if (existingApp != null) + { + AppPanel = existingApp.gameObject; + SetupExistingAppPanel(AppPanel, logger); + } + else + { + Transform templateApp = appsCanvas.transform.Find("ProductManagerApp"); + if (templateApp == null) + { + logger.Error("Template ProductManagerApp not found."); + yield break; + } + + AppPanel = Object.Instantiate(templateApp.gameObject, appsCanvas.transform); + AppPanel.name = AppName; + + Transform containerTransform = AppPanel.transform.Find("Container"); + if (containerTransform != null) + { + GameObject container = containerTransform.gameObject; + ClearContainer(container); + OnCreated(container); + } + + AppCreated = true; + } + + AppPanel.SetActive(false); + + if (!IconModified) + { + IconModified = ModifyAppIcon(IconLabel, IconFileName, logger); + if (IconModified) + logger.Msg("Icon modified."); + } + } + + /// + /// Sets up an existing app panel found in the scene (likely reused from a previous session). + /// + private void SetupExistingAppPanel(GameObject panel, MelonLogger.Instance logger) + { + Transform containerTransform = panel.transform.Find("Container"); + if (containerTransform != null) + { + GameObject container = containerTransform.gameObject; + if (container.transform.childCount < 2) + { + ClearContainer(container); + BuildUI(container); + } + } + + AppCreated = true; + } + + /// + /// Destroys all children of the app container to prepare for UI rebuilding. + /// + private void ClearContainer(GameObject container) + { + for (int i = container.transform.childCount - 1; i >= 0; i--) + Object.Destroy(container.transform.GetChild(i).gameObject); + } + + /// + /// Attempts to clone an app icon from the home screen and customize it. + /// + private bool ModifyAppIcon(string labelText, string fileName, MelonLogger.Instance logger) + { + GameObject parent = GameObject.Find("Player_Local/CameraContainer/Camera/OverlayCamera/GameplayMenu/Phone/phone/HomeScreen/AppIcons/"); + if (parent == null) + { + logger?.Error("AppIcons not found."); + return false; + } + + Transform lastIcon = parent.transform.childCount > 0 ? parent.transform.GetChild(parent.transform.childCount - 1) : null; + if (lastIcon == null) + { + logger?.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, logger); + } + + /// + /// Loads the icon PNG from disk and applies it to the cloned icon. + /// + private bool ChangeAppIconImage(GameObject iconObj, string filename, MelonLogger.Instance logger) + { + Transform imageTransform = iconObj.transform.Find("Mask/Image"); + Image image = imageTransform?.GetComponent(); + if (image == null) + { + logger?.Error("Image component not found in icon."); + return false; + } + + string path = Path.Combine(MelonEnvironment.UserDataDirectory, filename); + if (!File.Exists(path)) + { + logger?.Error("Icon file not found: " + path); + return false; + } + + try + { + byte[] bytes = File.ReadAllBytes(path); + Texture2D tex = new Texture2D(2, 2); + + if (tex.LoadImage(bytes)) // IL2CPP-safe overload + { + image.sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f)); + return true; + } + + Object.Destroy(tex); + } + catch (System.Exception e) + { + logger?.Error("Failed to load image: " + e.Message); + } + + return false; + } + } +} diff --git a/S1API/PhoneApp/PhoneAppManager.cs b/S1API/PhoneApp/PhoneAppManager.cs new file mode 100644 index 00000000..073fef9e --- /dev/null +++ b/S1API/PhoneApp/PhoneAppManager.cs @@ -0,0 +1,46 @@ +using System.Collections; +using System.Collections.Generic; +using MelonLoader; +using UnityEngine; + +namespace S1API.PhoneApp +{ + /// + /// Central manager for spawning phone apps. + /// Usage: PhoneAppManager.Register(new MyCustomApp()); + /// + public static class PhoneAppManager + { + private static readonly List registeredApps = new List(); + private static bool initialized = false; + + /// + /// Register your custom app. Should be called from OnApplicationStart(). + /// + public static void Register(PhoneApp app) + { + registeredApps.Add(app); + } + + /// + /// Call this once after the game scene is loaded. + /// Automatically initializes all registered apps. + /// + public static void InitAll(MelonLogger.Instance logger) + { + if (initialized) return; + initialized = true; + MelonCoroutines.Start(DelayedInitAll(logger)); + } + + private static IEnumerator DelayedInitAll(MelonLogger.Instance logger) + { + yield return new WaitForSeconds(5f); + + foreach (var app in registeredApps) + { + app.Init(logger); + } + } + } +} diff --git a/S1API/PhoneApp/UIFactory.cs b/S1API/PhoneApp/UIFactory.cs new file mode 100644 index 00000000..2e9a9d43 --- /dev/null +++ b/S1API/PhoneApp/UIFactory.cs @@ -0,0 +1,138 @@ +using UnityEngine; +using UnityEngine.UI; + +public static class UIFactory +{ + /// + /// Creates a background panel with optional anchors. + /// + public static GameObject Panel(string name, Transform parent, Color bgColor, Vector2? anchorMin = null, Vector2? anchorMax = null, bool fullAnchor = false) + { + GameObject go = new GameObject(name); + go.transform.SetParent(parent, false); + RectTransform rt = go.AddComponent(); + + if (fullAnchor) + { + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + } + else + { + rt.anchorMin = anchorMin ?? Vector2.zero; + rt.anchorMax = anchorMax ?? Vector2.one; + rt.offsetMin = Vector2.zero; + rt.offsetMax = Vector2.zero; + } + + Image bg = go.AddComponent(); + bg.color = bgColor; + + return go; + } + + /// + /// Creates a UI text element. + /// + public static Text Text(string name, string content, Transform parent, int fontSize = 16, TextAnchor anchor = TextAnchor.UpperLeft, FontStyle style = FontStyle.Normal) + { + GameObject go = new GameObject(name); + go.transform.SetParent(parent, false); + + RectTransform rt = go.AddComponent(); + rt.sizeDelta = new Vector2(0f, 30f); + + Text txt = go.AddComponent(); + txt.text = content; + txt.font = Resources.GetBuiltinResource("Arial.ttf"); + txt.fontSize = fontSize; + txt.alignment = anchor; + txt.fontStyle = style; + txt.color = Color.white; + + return txt; + } + + /// + /// Creates a button with label and background color. + /// + public static GameObject Button(string name, string label, Transform parent, Color color) + { + GameObject buttonGO = new GameObject(name); + buttonGO.transform.SetParent(parent, false); + + RectTransform rt = buttonGO.AddComponent(); + rt.sizeDelta = new Vector2(0, 40); + + Image img = buttonGO.AddComponent(); + img.color = color; + + Button btn = buttonGO.AddComponent