feat: implement custom UI replacements for main menu, pause menu, and shop via Harmony patching

This commit is contained in:
Marvin
2026-04-14 23:57:31 +02:00
parent ee6a14d46f
commit 551f9a0b53
10 changed files with 465 additions and 77 deletions
Binary file not shown.
+25 -4
View File
@@ -25,10 +25,17 @@ public class GregMainMenuReplacement : MonoBehaviour
private bool _isVisible = false;
private float _scaleFactor = 1f;
private Action _onPlayClicked;
private Action _onContinueClicked;
private Action _onNewGameClicked;
private Action _onLoadGameClicked;
private Action _onSettingsClicked;
private Action _onModsClicked;
private Action _onQuitClicked;
private Action _onReportBugClicked;
private Action _onDiscordClicked;
private Action _onWishlistClicked;
private Action _onTwitterClicked;
private Action _onStatsClicked;
private float _animTime = 0f;
private const float ANIM_DURATION = 0.6f;
@@ -43,12 +50,19 @@ public class GregMainMenuReplacement : MonoBehaviour
gameObject.SetActive(false);
}
public void Configure(Action onPlay, Action onSettings, Action onMods, Action onQuit)
public void Configure(Action onContinue, Action onNewGame, Action onLoadGame, Action onSettings, Action onMods, Action onQuit, Action onReportBug, Action onDiscord, Action onWishlist, Action onTwitter, Action onStats)
{
_onPlayClicked = onPlay;
_onContinueClicked = onContinue;
_onNewGameClicked = onNewGame;
_onLoadGameClicked = onLoadGame;
_onSettingsClicked = onSettings;
_onModsClicked = onMods;
_onQuitClicked = onQuit;
_onReportBugClicked = onReportBug;
_onDiscordClicked = onDiscord;
_onWishlistClicked = onWishlist;
_onTwitterClicked = onTwitter;
_onStatsClicked = onStats;
}
public void Show()
@@ -286,9 +300,16 @@ public class GregMainMenuReplacement : MonoBehaviour
float btnHeight = 56f * _scaleFactor;
CreateMenuButton("PLAY", GregButtonStyle.Primary, btnHeight, () => _onPlayClicked?.Invoke());
CreateMenuButton("CONTINUE", GregButtonStyle.Primary, btnHeight, () => _onContinueClicked?.Invoke());
CreateMenuButton("NEW GAME", GregButtonStyle.Secondary, btnHeight, () => _onNewGameClicked?.Invoke());
CreateMenuButton("LOAD GAME", GregButtonStyle.Secondary, btnHeight, () => _onLoadGameClicked?.Invoke());
CreateMenuButton("SETTINGS", GregButtonStyle.Secondary, btnHeight, () => _onSettingsClicked?.Invoke());
CreateMenuButton("MODS", GregButtonStyle.Secondary, btnHeight, () => _onModsClicked?.Invoke());
CreateMenuButton("REPORT BUG", GregButtonStyle.Secondary, btnHeight, () => _onReportBugClicked?.Invoke());
CreateMenuButton("DISCORD", GregButtonStyle.Secondary, btnHeight, () => _onDiscordClicked?.Invoke());
CreateMenuButton("WISHLIST", GregButtonStyle.Secondary, btnHeight, () => _onWishlistClicked?.Invoke());
CreateMenuButton("TWITTER", GregButtonStyle.Secondary, btnHeight, () => _onTwitterClicked?.Invoke());
CreateMenuButton("STATS", GregButtonStyle.Secondary, btnHeight, () => _onStatsClicked?.Invoke());
CreateMenuButton("QUIT", GregButtonStyle.Danger, btnHeight, () => _onQuitClicked?.Invoke());
}
@@ -0,0 +1,91 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Il2CppTMPro;
using greg.Core.UI;
using greg.Core.UI.Components;
namespace greg.Core.UI.Components;
public class GregPauseMenuReplacement : MonoBehaviour
{
public static GregPauseMenuReplacement Instance { get; private set; }
private GameObject _root;
private GregPanel _mainPanel;
private bool _isVisible = false;
private Action _onResumeClicked;
private Action _onSettingsClicked;
private Action _onSaveClicked;
private Action _onLoadClicked;
private Action _onModsClicked;
private Action _onQuitToMenuClicked;
private Action _onQuitToDesktopClicked;
private void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeUI();
gameObject.SetActive(false);
}
public void Configure(Action onResume, Action onSettings, Action onSave, Action onLoad, Action onMods, Action onQuitToMenu, Action onQuitToDesktop)
{
_onResumeClicked = onResume;
_onSettingsClicked = onSettings;
_onSaveClicked = onSave;
_onLoadClicked = onLoad;
_onModsClicked = onMods;
_onQuitToMenuClicked = onQuitToMenu;
_onQuitToDesktopClicked = onQuitToDesktop;
}
private void InitializeUI()
{
_mainPanel = GregUIBuilder.Panel("pause_menu_replacement")
.Title("PAUSE")
.Position(GregUIAnchor.Center)
.Size(400, 550)
.Closable(false)
.Draggable(false)
.AddButton("RESUME", () => _onResumeClicked?.Invoke(), GregButtonStyle.Primary)
.AddSeparator()
.AddButton("SETTINGS", () => _onSettingsClicked?.Invoke(), GregButtonStyle.Secondary)
.AddButton("SAVE GAME", () => _onSaveClicked?.Invoke(), GregButtonStyle.Secondary)
.AddButton("LOAD GAME", () => _onLoadClicked?.Invoke(), GregButtonStyle.Secondary)
.AddButton("MODS", () => _onModsClicked?.Invoke(), GregButtonStyle.Secondary)
.AddSeparator()
.AddButton("MAIN MENU", () => _onQuitToMenuClicked?.Invoke(), GregButtonStyle.Danger)
.AddButton("EXIT TO DESKTOP", () => _onQuitToDesktopClicked?.Invoke(), GregButtonStyle.Danger)
.Build();
if (_mainPanel.PanelRoot != null)
{
_mainPanel.PanelRoot.transform.SetParent(this.transform, false);
}
}
public void Show()
{
_isVisible = true;
gameObject.SetActive(true);
_mainPanel.Show();
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
public void Hide()
{
_isVisible = false;
_mainPanel.Hide();
gameObject.SetActive(false);
}
public void Toggle()
{
if (_isVisible) Hide();
else Show();
}
}
+76
View File
@@ -0,0 +1,76 @@
using System;
using UnityEngine;
using UnityEngine.UI;
using Il2CppTMPro;
using greg.Core.UI;
using greg.Core.UI.Components;
namespace greg.Core.UI.Components;
public class GregShopReplacement : MonoBehaviour
{
public static GregShopReplacement Instance { get; private set; }
private GameObject _root;
private GregPanel _mainPanel;
private bool _isVisible = false;
private Action _onCloseClicked;
private Action _onCheckoutClicked;
private void Awake()
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeUI();
gameObject.SetActive(false);
}
public void Configure(Action onClose, Action onCheckout)
{
_onCloseClicked = onClose;
_onCheckoutClicked = onCheckout;
}
private void InitializeUI()
{
_mainPanel = GregUIBuilder.Panel("shop_replacement")
.Title("🛒 COMPUTER SHOP")
.Position(GregUIAnchor.Center)
.Size(800, 600)
.AddLabel("Select hardware components for your Data Center / Wählen Sie Hardware für Ihr Rechenzentrum", null, 14)
.AddSeparator()
.AddLabel("(Hardware inventory loading...)")
.AddSeparator()
.AddButton("CHECKOUT", () => _onCheckoutClicked?.Invoke(), GregButtonStyle.Primary)
.AddButton("CLOSE", () => _onCloseClicked?.Invoke(), GregButtonStyle.Secondary)
.Build();
if (_mainPanel.PanelRoot != null)
{
_mainPanel.PanelRoot.transform.SetParent(this.transform, false);
}
}
public void Show()
{
_isVisible = true;
gameObject.SetActive(true);
_mainPanel.Show();
Cursor.visible = true;
Cursor.lockState = CursorLockMode.None;
}
public void Hide()
{
_isVisible = false;
_mainPanel.Hide();
gameObject.SetActive(false);
}
public void Toggle()
{
if (_isVisible) Hide();
else Show();
}
}
+60 -15
View File
@@ -180,25 +180,54 @@ public static class GameUIButtons
{
public static bool ClickButton(string buttonPath)
{
var button = GameObject.Find(buttonPath);
if (button == null)
{
MelonLogger.Warning($"[GameUIButtons] Button not found: {buttonPath}");
return false;
}
var button = FindButton(buttonPath);
if (button == null) return false;
var btn = button.GetComponent<UnityEngine.UI.Button>();
if (btn == null)
{
MelonLogger.Warning($"[GameUIButtons] No Button component: {buttonPath}");
return false;
}
btn.onClick?.Invoke();
button.onClick?.Invoke();
MelonLogger.Msg($"[GameUIButtons] Clicked: {buttonPath}");
return true;
}
private static UnityEngine.UI.Button FindButton(string path)
{
var go = GameObject.Find(path);
if (go != null) return go.GetComponent<UnityEngine.UI.Button>();
// If not found (likely inactive), check all buttons
var allButtons = Resources.FindObjectsOfTypeAll<UnityEngine.UI.Button>();
foreach (var btn in allButtons)
{
if (GetGameObjectPath(btn.gameObject).Contains(path)) return btn;
}
return null;
}
private static string GetGameObjectPath(GameObject obj)
{
string path = "/" + obj.name;
while (obj.transform.parent != null)
{
obj = obj.transform.parent.gameObject;
path = "/" + obj.name + path;
}
return path;
}
public static bool ClickButtonByName(string buttonName)
{
var allButtons = Resources.FindObjectsOfTypeAll<UnityEngine.UI.Button>();
foreach (var btn in allButtons)
{
if (btn.name == buttonName)
{
btn.onClick?.Invoke();
MelonLogger.Msg($"[GameUIButtons] Clicked by name: {buttonName}");
return true;
}
}
return false;
}
public static bool ClickButtonInCanvas(string canvasName, string buttonName)
{
return ClickButton($"{canvasName}/{buttonName}");
@@ -206,7 +235,17 @@ public static class GameUIButtons
public static bool ClickLoadButton()
{
return ClickButton("PauseMenuCanvas/Pause menu - Settings Scripts/PanelArea/PanelBackground/SystemPanel/Game/LoadButton");
return ClickButtonByName("LoadGame") || ClickButton("Canvas - MainMenuScript/HorizontalLayout/MainMenu/LoadGame") || ClickButton("PauseMenuCanvas/Pause menu - Settings Scripts/PanelArea/PanelBackground/SystemPanel/Game/LoadButton");
}
public static bool ClickNewGameButton()
{
return ClickButtonByName("NewGame") || ClickButton("Canvas - MainMenuScript/HorizontalLayout/MainMenu/NewGame");
}
public static bool ClickContinueButton()
{
return ClickButtonByName("Continue") || ClickButton("Canvas - MainMenuScript/HorizontalLayout/MainMenu/Continue");
}
public static bool ClickSaveButton()
@@ -223,4 +262,10 @@ public static class GameUIButtons
{
return ClickButton("PauseMenuCanvas/Pause menu - Settings Scripts/PanelArea/PanelBackground/SystemPanel/Game/MainMenuButton");
}
public static bool ClickReportBugButton() => ClickButtonByName("Report Bug") || ClickButton("Canvas - MainMenuScript/HorizontalLayout/MainMenu/Report Bug");
public static bool ClickDiscordButton() => ClickButtonByName("Discord") || ClickButton("Canvas - MainMenuScript/TopRight_VerticalLayout/Discord");
public static bool ClickWishlistButton() => ClickButtonByName("Wishlist") || ClickButton("Canvas - MainMenuScript/TopRight_VerticalLayout/Wishlist");
public static bool ClickTwitterButton() => ClickButton("Canvas - MainMenuScript/Twitter/Button");
public static bool ClickStatsButton() => ClickButtonByName("SteamStats") || ClickButton("Canvas - MainMenuScript/Leaderboards and stats/SteamStats");
}
+59
View File
@@ -0,0 +1,59 @@
using System;
using HarmonyLib;
using Il2Cpp;
using UnityEngine;
using greg.Core.UI;
using greg.Core.UI.Components;
namespace greg.Harmony;
[HarmonyPatch(typeof(ComputerShop), "OnEnable")]
public static class GregShopPatch
{
static void Postfix(ComputerShop __instance)
{
MelonLoader.MelonLogger.Msg("[gregCore] ComputerShop detected. Switching to modernized UI...");
// Disable original UI siblings/children safely
foreach (Transform child in __instance.transform)
{
child.gameObject.SetActive(false);
}
UIRouter.SetMode(UIMode.ComputerShop);
InitializeReplacement();
}
private static void InitializeReplacement()
{
if (GregShopReplacement.Instance != null) return;
var go = new GameObject("GregShop_Root");
var comp = go.AddComponent<GregShopReplacement>();
comp.Configure(
onClose: OnCloseClicked,
onCheckout: OnCheckoutClicked
);
}
private static void OnCloseClicked()
{
GregShopReplacement.Instance?.Hide();
UIRouter.SetMode(UIMode.Playing);
// Find the Shop original instance and close it if necessary
// In most games, disabling the UI or calling a Close method works.
var shop = UnityEngine.Object.FindObjectOfType<ComputerShop>();
if (shop != null)
{
// Trigger original close logic if it exists, or just ensure mode is back
GameUIButtons.ClickButtonByName("CloseShop");
}
}
private static void OnCheckoutClicked()
{
MelonLoader.MelonLogger.Msg("[Shop] Checkout clicked");
GameUIButtons.ClickButtonByName("ButtonCheckOut");
}
}
+61 -12
View File
@@ -35,31 +35,50 @@ public static class MainMenuController
_menuInstance = AddComponentSafe<GregMainMenuReplacement>(go);
_menuInstance?.Configure(
onPlay: OnPlayClicked,
onContinue: OnContinueClicked,
onNewGame: OnNewGameClicked,
onLoadGame: OnLoadGameClicked,
onSettings: OnSettingsClicked,
onMods: OnModsClicked,
onQuit: OnQuitClicked
onQuit: OnQuitClicked,
onReportBug: OnReportBugClicked,
onDiscord: OnDiscordClicked,
onWishlist: OnWishlistClicked,
onTwitter: OnTwitterClicked,
onStats: OnStatsClicked
);
}
public static void Show() => _menuInstance?.Show();
public static void Hide() => _menuInstance?.Hide();
private static void OnPlayClicked()
private static void OnContinueClicked()
{
MelonLogger.Msg("[MainMenu] Play clicked");
MelonLogger.Msg("[MainMenu] Continue clicked");
Hide();
if (GameUIButtons.ClickContinueButton())
{
UIRouter.SetMode(UIMode.Playing);
}
}
private static void OnNewGameClicked()
{
MelonLogger.Msg("[MainMenu] New Game clicked");
Hide();
if (GameUIButtons.ClickNewGameButton())
{
UIRouter.SetMode(UIMode.Playing);
}
}
private static void OnLoadGameClicked()
{
MelonLogger.Msg("[MainMenu] Load Game clicked");
if (GameUIButtons.ClickLoadButton())
{
MelonLogger.Msg("[MainMenu] Load button invoked via reflection");
Hide();
}
else
{
GameMethodInvoker.Invoke(GameUIElements.Types.MainGameManager, "LoadGame");
}
UIRouter.SetMode(UIMode.Playing);
}
private static void OnSettingsClicked()
@@ -76,6 +95,36 @@ public static class MainMenuController
gregModConfigManager.Toggle(true);
}
private static void OnReportBugClicked()
{
MelonLogger.Msg("[MainMenu] Report Bug clicked");
GameUIButtons.ClickReportBugButton();
}
private static void OnDiscordClicked()
{
MelonLogger.Msg("[MainMenu] Discord clicked");
GameUIButtons.ClickDiscordButton();
}
private static void OnWishlistClicked()
{
MelonLogger.Msg("[MainMenu] Wishlist clicked");
GameUIButtons.ClickWishlistButton();
}
private static void OnTwitterClicked()
{
MelonLogger.Msg("[MainMenu] Twitter clicked");
GameUIButtons.ClickTwitterButton();
}
private static void OnStatsClicked()
{
MelonLogger.Msg("[MainMenu] Stats clicked");
GameUIButtons.ClickStatsButton();
}
private static void OnQuitClicked()
{
MelonLogger.Msg("[MainMenu] Quit clicked");
+74 -45
View File
@@ -3,7 +3,9 @@ using HarmonyLib;
using Il2Cpp;
using UnityEngine;
using UnityEngine.UI;
using MelonLoader;
using greg.Core.UI;
using greg.Core.UI.Components;
namespace greg.Harmony;
@@ -13,53 +15,79 @@ public static class PauseMenuPatch
static void Postfix(PauseMenu __instance)
{
UIRouter.SetMode(UIMode.Paused);
MelonLoader.MelonCoroutines.Start(DelayedInjection(__instance));
}
private static IEnumerator DelayedInjection(PauseMenu menu)
{
yield return new WaitForSeconds(0.5f);
InjectModsButton(menu);
}
public static void InjectModsButton(PauseMenu menu)
{
try
InitializeReplacement();
// Hide original UI elements safely
if (__instance.gameObject.activeSelf)
{
if (GameObject.Find("greg_PauseModsButton") != null) return;
GameObject resumeBtn = menu.resumeButton;
if (resumeBtn == null) return;
GameObject modsButtonGo = Object.Instantiate(resumeBtn, resumeBtn.transform.parent);
modsButtonGo.name = "greg_PauseModsButton";
var button = modsButtonGo.GetComponent<Button>();
if (button != null)
{
button.onClick = new Button.ButtonClickedEvent();
button.onClick.RemoveAllListeners();
button.onClick.AddListener((System.Action)(() => {
gregModConfigManager.Toggle(true);
}));
}
var img = modsButtonGo.GetComponent<Image>();
if (img != null) img.color = new Color(0.00f, 0.07f, 0.06f, 0.85f);
var textComp = modsButtonGo.GetComponentInChildren<TextMeshProUGUI>();
if (textComp != null) {
textComp.text = "MODS";
textComp.color = new Color(0.38f, 0.96f, 0.85f, 1f);
}
modsButtonGo.transform.SetSiblingIndex(resumeBtn.transform.parent.childCount - 1);
MelonLoader.MelonLogger.Msg("[gregCore] MODS button injected into Pause Menu.");
}
catch (System.Exception ex) {
greg.Core.CrashLog.LogException("PauseMenuPatch.InjectModsButton", ex);
// We can't disable the object itself because it might break game logic
// but we can disable the visual child elements.
foreach(Transform child in __instance.transform)
{
child.gameObject.SetActive(false);
}
}
GregPauseMenuReplacement.Instance?.Show();
}
private static void InitializeReplacement()
{
if (GregPauseMenuReplacement.Instance != null) return;
var go = new GameObject("GregPauseMenu_Root");
var comp = go.AddComponent<GregPauseMenuReplacement>();
comp.Configure(
onResume: OnResumeClicked,
onSettings: OnSettingsClicked,
onSave: OnSaveClicked,
onLoad: OnLoadClicked,
onMods: OnModsClicked,
onQuitToMenu: OnQuitToMenuClicked,
onQuitToDesktop: OnQuitToDesktopClicked
);
}
private static void OnResumeClicked()
{
GregPauseMenuReplacement.Instance?.Hide();
UIRouter.SetMode(UIMode.Playing);
// Find the PauseMenu original instance and call Resume
var menu = UnityEngine.Object.FindObjectOfType<PauseMenu>();
menu?.Resume();
}
private static void OnSettingsClicked()
{
MelonLogger.Msg("[PauseMenu] Settings clicked");
}
private static void OnSaveClicked()
{
MelonLogger.Msg("[PauseMenu] Save clicked");
GameUIButtons.ClickSaveButton();
}
private static void OnLoadClicked()
{
MelonLogger.Msg("[PauseMenu] Load clicked");
GameUIButtons.ClickLoadButton();
}
private static void OnModsClicked()
{
gregModConfigManager.Toggle(true);
}
private static void OnQuitToMenuClicked()
{
MelonLogger.Msg("[PauseMenu] Quit to Menu clicked");
GameUIButtons.ClickButtonByName("QuitToMenu");
}
private static void OnQuitToDesktopClicked()
{
Application.Quit();
}
}
@@ -68,6 +96,7 @@ public static class PauseMenuResumePatch
{
static void Postfix()
{
GregPauseMenuReplacement.Instance?.Hide();
UIRouter.SetMode(UIMode.Playing);
}
}
+18
View File
@@ -28,6 +28,24 @@ public static class UIRouterHooks
{
GregMainMenuReplacement.Instance?.Hide();
}
if (to == UIMode.Paused)
{
GregPauseMenuReplacement.Instance?.Show();
}
else
{
GregPauseMenuReplacement.Instance?.Hide();
}
if (to == UIMode.ComputerShop)
{
GregShopReplacement.Instance?.Show();
}
else
{
GregShopReplacement.Instance?.Hide();
}
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ public abstract class gregModBase : MelonMod
/// Gets the list of optional assembly/DLL names (without extension).
/// If missing, a warning is logged but the mod still initializes.
/// </summary>
public virtual string[] OptionalDependencies => Array.Empty<string>();
public new virtual string[] OptionalDependencies => Array.Empty<string>();
/// <summary>
/// Indicates if the dependency check failed.