diff --git a/HAModHelper.GamePlugin/Base/Base.GamePlugin.cs b/HAModHelper.GamePlugin/Base/Base.GamePlugin.cs
index e999a2d..c9252dd 100644
--- a/HAModHelper.GamePlugin/Base/Base.GamePlugin.cs
+++ b/HAModHelper.GamePlugin/Base/Base.GamePlugin.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using HAModHelper.GamePlugin.Items.Systems;
using HAModHelper.GamePlugin.Perks.Systems;
+using HAModHelper.GamePlugin.Gui.Systems;
using HAModHelper.GamePlugin.Dialogue.Systems;
using HAModHelper.GamePlugin.Core.Debug;
using System.Net;
@@ -84,9 +85,14 @@ public override void Load()
Log.LogInfo($"[HAMH] Initialized CraftingInjectionManager in {stopwatch4.ElapsedMilliseconds}ms.");
var stopwatch5 = Stopwatch.StartNew();
- DialogueManager.Instance.Initialize();
+ WindowManager.Instance.Initialize();
stopwatch5.Stop();
- Log.LogInfo($"[HAMH] Initialized DialogueManager in {stopwatch5.ElapsedMilliseconds}ms.");
+ Log.LogInfo($"[HAMH] Initialized WindowManager in {stopwatch5.ElapsedMilliseconds}ms.");
+
+ var stopwatch6 = Stopwatch.StartNew();
+ DialogueManager.Instance.Initialize();
+ stopwatch6.Stop();
+ Log.LogInfo($"[HAMH] Initialized DialogueManager in {stopwatch6.ElapsedMilliseconds}ms.");
//var stopwatch3 = Stopwatch.StartNew();
//UniverseLibConfig uvlconfig = new UniverseLibConfig
diff --git a/HAModHelper.GamePlugin/Gui/Gui.Events.cs b/HAModHelper.GamePlugin/Gui/Gui.Events.cs
new file mode 100644
index 0000000..6a4b065
--- /dev/null
+++ b/HAModHelper.GamePlugin/Gui/Gui.Events.cs
@@ -0,0 +1,15 @@
+using HAModHelper.GamePlugin.Base.Events;
+
+namespace HAModHelper.GamePlugin.Gui.Events;
+
+public class CustomScreenOpenedEvent : BaseEvent
+{
+ public string ScreenId { get; }
+ public CustomScreenOpenedEvent(string screenId) => ScreenId = screenId;
+}
+
+public class CustomScreenClosedEvent : BaseEvent
+{
+ public string ScreenId { get; }
+ public CustomScreenClosedEvent(string screenId) => ScreenId = screenId;
+}
diff --git a/HAModHelper.GamePlugin/Gui/Gui.Interfaces.cs b/HAModHelper.GamePlugin/Gui/Gui.Interfaces.cs
new file mode 100644
index 0000000..1f353e4
--- /dev/null
+++ b/HAModHelper.GamePlugin/Gui/Gui.Interfaces.cs
@@ -0,0 +1,57 @@
+using UnityEngine;
+
+namespace HAModHelper.GamePlugin.Gui.Interfaces;
+
+public interface IWindowPrefabsControl
+{
+ Transform? GuiCanvasTransform { get; }
+
+ GameObject? GetScreen(string screenId);
+
+ GameObject? GetObject(string screenId, string objName);
+
+ bool HasScreen(string screenId);
+
+ /// Register an already-instantiated screen so the game's own GetScreen/GetObject calls find it too.
+ void RegisterInstantiated(string screenId, GameObject instance);
+
+ void DestroyScreen(string screenId);
+}
+
+// runtime adapter that wraps the real game WindowPrefabsControl/WindowControl
+public class UnityWindowPrefabsControl : IWindowPrefabsControl
+{
+ private readonly WindowPrefabsControl _wpc;
+ private readonly WindowControl _wc;
+
+ public UnityWindowPrefabsControl(WindowPrefabsControl wpc, WindowControl wc)
+ {
+ _wpc = wpc;
+ _wc = wc;
+ }
+
+ public Transform? GuiCanvasTransform => _wc.gui_canvas != null ? _wc.gui_canvas.transform : null;
+
+ public GameObject? GetScreen(string screenId) => _wpc.GetScreen(screenId);
+
+ public GameObject? GetObject(string screenId, string objName) => _wpc.GetObject(screenId, objName);
+
+ public bool HasScreen(string screenId) => _wpc.prefab_screens_instantiated.ContainsKey(screenId);
+
+ public void RegisterInstantiated(string screenId, GameObject instance)
+ {
+ _wpc.prefab_screens_instantiated[screenId] = instance;
+ }
+
+ public void DestroyScreen(string screenId) => _wpc.DestroyScreen(screenId);
+}
+
+public class DebugNoLoadWindowPrefabsControl : IWindowPrefabsControl
+{
+ public Transform? GuiCanvasTransform => null;
+ public GameObject? GetScreen(string screenId) => null;
+ public GameObject? GetObject(string screenId, string objName) => null;
+ public bool HasScreen(string screenId) => false;
+ public void RegisterInstantiated(string screenId, GameObject instance) { }
+ public void DestroyScreen(string screenId) { }
+}
diff --git a/HAModHelper.GamePlugin/Gui/Gui.Systems.cs b/HAModHelper.GamePlugin/Gui/Gui.Systems.cs
new file mode 100644
index 0000000..b6d4971
--- /dev/null
+++ b/HAModHelper.GamePlugin/Gui/Gui.Systems.cs
@@ -0,0 +1,148 @@
+using HAModHelper.GamePlugin.Core;
+using HAModHelper.GamePlugin.Base.Events;
+using HAModHelper.GamePlugin.Gui.Events;
+using HAModHelper.GamePlugin.Gui.Interfaces;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace HAModHelper.GamePlugin.Gui.Systems;
+
+///
+/// Lets mods register their own AssetBundle-backed UI screens and show/hide them under
+/// the game's main GUI canvas. Registered screens are written into
+/// WindowPrefabsControl.prefab_screens_instantiated so the game's own
+/// GetScreen/GetObject/DestroyScreen calls keep working on them too —
+/// mirrors how injects custom world prefabs.
+///
+public sealed class WindowManager
+{
+ public static WindowManager Instance { get; } = new();
+
+ private sealed class ScreenEntry
+ {
+ public required AssetBundle Bundle;
+ public required string AssetName;
+ public GameObject? Cache;
+ }
+
+ private readonly Dictionary _screens = new();
+
+ // TEST-ONLY: Spoof a fake WindowPrefabsControl for tests
+ public IWindowPrefabsControl? DebugWindowPrefabsControlSource { get; set; }
+
+ private WindowManager() { }
+
+ private IWindowPrefabsControl? GetControl()
+ {
+ if (DebugWindowPrefabsControlSource?.GetType() == typeof(DebugNoLoadWindowPrefabsControl))
+ return null;
+
+ if (DebugWindowPrefabsControlSource != null)
+ return DebugWindowPrefabsControlSource;
+
+ try
+ {
+ var wpc = WindowPrefabsControl.Instance;
+ var wc = WindowControl.Instance;
+ if (wpc == null || wc == null) return null;
+ var proxy = new UnityWindowPrefabsControl(wpc, wc);
+ return proxy.GuiCanvasTransform == null ? null : proxy;
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ /// TEST-ONLY: Reset system state.
+ public void Reset()
+ {
+ _screens.Clear();
+ DebugWindowPrefabsControlSource = null;
+ }
+
+ /// Initialize the window manager (called on game start).
+ public void Initialize()
+ {
+ }
+
+ ///
+ /// Register a custom screen prefab.
+ ///
+ /// Unique id for the screen, used with etc.
+ /// AssetBundle that contains the prefab. Keep a static reference
+ /// in your plugin so the native backing is never freed.
+ /// Name of the prefab asset inside the bundle.
+ public void RegisterScreen(string screenId, AssetBundle bundle, string assetName)
+ {
+ _screens[screenId] = new ScreenEntry { Bundle = bundle, AssetName = assetName };
+ }
+
+ ///
+ /// Instantiate (first call) or re-show a registered custom screen, parented under the
+ /// game's main GUI canvas. Returns null if the screen isn't registered or the game's
+ /// window system isn't loaded yet.
+ ///
+ public GameObject? ShowScreen(string screenId)
+ {
+ var control = GetControl();
+ if (control == null)
+ {
+ try { HAMHMod.Logger.LogInfo($"[HAMH] WindowPrefabsControl not ready, can't show screen {screenId}"); } catch { }
+ return null;
+ }
+
+ var existing = control.GetScreen(screenId);
+ if (existing != null)
+ {
+ existing.SetActive(true);
+ EventBus.Instance.Fire(new CustomScreenOpenedEvent(screenId));
+ return existing;
+ }
+
+ if (!_screens.TryGetValue(screenId, out var entry))
+ {
+ try { HAMHMod.Logger.LogInfo($"[HAMH] No custom screen registered for {screenId}"); } catch { }
+ return null;
+ }
+
+ if (entry.Cache == null)
+ {
+ var asset = entry.Bundle.LoadAsset(entry.AssetName);
+ if (asset == null) return null;
+ entry.Cache = asset.Cast();
+ }
+
+ var instance = Object.Instantiate(entry.Cache, control.GuiCanvasTransform);
+ instance.SetActive(true);
+ control.RegisterInstantiated(screenId, instance);
+
+ EventBus.Instance.Fire(new CustomScreenOpenedEvent(screenId));
+ return instance;
+ }
+
+ /// Hide a shown custom (or base-game) screen without destroying it.
+ public bool HideScreen(string screenId)
+ {
+ var existing = GetControl()?.GetScreen(screenId);
+ if (existing == null) return false;
+
+ existing.SetActive(false);
+ EventBus.Instance.Fire(new CustomScreenClosedEvent(screenId));
+ return true;
+ }
+
+ /// Destroy a shown custom (or base-game) screen entirely.
+ public bool DestroyScreen(string screenId)
+ {
+ var control = GetControl();
+ if (control == null || !control.HasScreen(screenId)) return false;
+
+ control.DestroyScreen(screenId);
+ EventBus.Instance.Fire(new CustomScreenClosedEvent(screenId));
+ return true;
+ }
+
+ /// Look up a child object inside a screen (custom or base-game) by name.
+ public GameObject? GetObject(string screenId, string objName) => GetControl()?.GetObject(screenId, objName);
+}