Event System

Updated 2 days ago

Event System

Learn how to use ZUI's event system to respond to UI changes and updates.


📋 Table of Contents


Overview

ZUI provides an event system that allows mods to respond to UI changes. Currently, the primary event is OnButtonsChanged, which fires when UI buttons are updated.

Available Events

Event Description When It Fires
OnButtonsChanged Triggered when UI buttons are added or updated After any plugin registers/updates buttons

Why Use Events?

Events are useful for:

  • 🔄 Refreshing UI state
  • 📊 Updating dynamic content
  • 🔔 Responding to UI changes from other mods
  • 🎯 Synchronizing multiple UI elements
  • 📈 Tracking UI modifications

OnButtonsChanged Event

The OnButtonsChanged event fires whenever buttons are registered or modified in the ZUI system.

Event Signature

public static event Action OnButtonsChanged;

Subscribing to the Event

Hard Dependency Approach

If using ZUI as a hard dependency:

using ZUI.API;

public class Plugin : BasePlugin
{
    public override void Load()
    {
        // Subscribe to the event
        ZUI.OnButtonsChanged += OnUIUpdated;
        
        Log.LogInfo("Subscribed to ZUI events");
    }

    private void OnUIUpdated()
    {
        Log.LogInfo("UI buttons were updated!");
        // Your response logic here
    }
}

Soft Dependency Approach (Recommended)

If using ZUI as a soft dependency with reflection:

private static Type _zuiType;
private static EventInfo _onButtonsChangedEvent;

private bool InitZUI()
{
    if (!IL2CPPChainloader.Instance.Plugins.ContainsKey("Zanakinz.ZUI"))
        return false;
        
    var assembly = AppDomain.CurrentDomain.GetAssemblies()
        .FirstOrDefault(a => a.GetName().Name == "ZUI");
    
    if (assembly == null) return false;
    
    _zuiType = assembly.GetType("ZUI.API.ZUI");
    
    if (_zuiType == null) return false;
    
    // Get the event
    _onButtonsChangedEvent = _zuiType.GetEvent("OnButtonsChanged", 
        BindingFlags.Public | BindingFlags.Static);
    
    return _onButtonsChangedEvent != null;
}

private void SubscribeToEvents()
{
    if (_onButtonsChangedEvent == null) return;
    
    try
    {
        // Create delegate for the event
        var handler = Delegate.CreateDelegate(
            _onButtonsChangedEvent.EventHandlerType,
            this,
            nameof(OnUIUpdated)
        );
        
        // Subscribe to event
        _onButtonsChangedEvent.AddEventHandler(null, handler);
        
        Log.LogInfo("Subscribed to OnButtonsChanged event");
    }
    catch (Exception ex)
    {
        Log.LogError($"Failed to subscribe to events: {ex.Message}");
    }
}

private void OnUIUpdated()
{
    Log.LogInfo("UI buttons were updated!");
    // Your response logic here
}

public override void Load()
{
    if (InitZUI())
    {
        RegisterUI();
        SubscribeToEvents();
    }
}

Unsubscribing from Events

Good practice to unsubscribe when your plugin unloads:

Hard Dependency

public override bool Unload()
{
    ZUI.OnButtonsChanged -= OnUIUpdated;
    Log.LogInfo("Unsubscribed from ZUI events");
    return true;
}

Soft Dependency

private Delegate _eventHandler;

private void SubscribeToEvents()
{
    if (_onButtonsChangedEvent == null) return;
    
    try
    {
        _eventHandler = Delegate.CreateDelegate(
            _onButtonsChangedEvent.EventHandlerType,
            this,
            nameof(OnUIUpdated)
        );
        
        _onButtonsChangedEvent.AddEventHandler(null, _eventHandler);
        Log.LogInfo("Subscribed to OnButtonsChanged event");
    }
    catch (Exception ex)
    {
        Log.LogError($"Failed to subscribe to events: {ex.Message}");
    }
}

public override bool Unload()
{
    if (_onButtonsChangedEvent != null && _eventHandler != null)
    {
        try
        {
            _onButtonsChangedEvent.RemoveEventHandler(null, _eventHandler);
            Log.LogInfo("Unsubscribed from ZUI events");
        }
        catch (Exception ex)
        {
            Log.LogError($"Failed to unsubscribe: {ex.Message}");
        }
    }
    return true;
}

Event Usage Patterns

Pattern 1: Update UI on Changes

Refresh your mod's UI when other mods make changes:

private void OnUIUpdated()
{
    Log.LogInfo("UI updated, refreshing my mod's display");
    UpdateMyModUI();
}

private void UpdateMyModUI()
{
    // Re-register or update your UI elements
    Call("SetPlugin", "MyMod");
    Call("SetTargetWindow", "MyPanel");
    
    // Update dynamic content
    var playerCount = GetPlayerCount();
    Call("AddText", $"Players Online: {playerCount}", 20f, 50f);
}

Pattern 2: Track UI State

Keep track of how many times the UI has been modified:

private int _uiUpdateCount = 0;

private void OnUIUpdated()
{
    _uiUpdateCount++;
    Log.LogInfo($"UI has been updated {_uiUpdateCount} times");
    
    // Perform action every N updates
    if (_uiUpdateCount % 10 == 0)
    {
        Log.LogInfo("UI updated 10 times, performing maintenance...");
        PerformUIMaintenanceTask();
    }
}

Pattern 3: Synchronize Multiple Windows

Keep multiple custom windows in sync:

private void OnUIUpdated()
{
    Log.LogInfo("Synchronizing all windows");
    
    // Update main panel
    UpdateMainPanel();
    
    // Update status panel
    UpdateStatusPanel();
    
    // Update settings panel
    UpdateSettingsPanel();
}

private void UpdateMainPanel()
{
    Call("SetTargetWindow", "MainPanel");
    // Update content...
}

private void UpdateStatusPanel()
{
    Call("SetTargetWindow", "StatusPanel");
    // Update content...
}

Pattern 4: Conditional Updates

Only respond to events under certain conditions:

private bool _isEnabled = true;
private DateTime _lastUpdate = DateTime.MinValue;

private void OnUIUpdated()
{
    // Ignore if disabled
    if (!_isEnabled)
    {
        Log.LogDebug("UI update ignored (disabled)");
        return;
    }
    
    // Rate limiting - only update once per second
    if ((DateTime.Now - _lastUpdate).TotalSeconds < 1.0)
    {
        Log.LogDebug("UI update ignored (rate limited)");
        return;
    }
    
    _lastUpdate = DateTime.Now;
    Log.LogInfo("Processing UI update");
    UpdateMyModUI();
}

Best Practices

✅ DO:

1. Keep event handlers lightweight

private void OnUIUpdated()
{
    // Quick, simple operations
    Log.LogInfo("UI updated");
    _needsRefresh = true;  // Set flag for later processing
}

2. Use error handling

private void OnUIUpdated()
{
    try
    {
        UpdateMyModUI();
    }
    catch (Exception ex)
    {
        Log.LogError($"Error in OnUIUpdated: {ex.Message}");
    }
}

3. Unsubscribe when done

public override bool Unload()
{
    ZUI.OnButtonsChanged -= OnUIUpdated;
    return true;
}

4. Use rate limiting if needed

private DateTime _lastEventTime = DateTime.MinValue;

private void OnUIUpdated()
{
    if ((DateTime.Now - _lastEventTime).TotalSeconds < 0.5)
        return;  // Ignore rapid-fire events
    
    _lastEventTime = DateTime.Now;
    // Process update...
}

❌ DON'T:

1. Don't do heavy processing in event handlers

// ❌ Bad
private void OnUIUpdated()
{
    // Expensive operation directly in event
    for (int i = 0; i < 10000; i++)
    {
        PerformExpensiveCalculation();
    }
}

// ✅ Good
private bool _needsUpdate = false;

private void OnUIUpdated()
{
    _needsUpdate = true;  // Set flag
}

public override void Update()
{
    if (_needsUpdate)
    {
        PerformExpensiveCalculation();
        _needsUpdate = false;
    }
}

2. Don't cause infinite loops

// ❌ Bad - This will cause infinite event loop!
private void OnUIUpdated()
{
    Call("AddButton", "Test", ".test");  // Triggers OnButtonsChanged again!
}

// ✅ Good - Use flag to prevent recursion
private bool _isUpdating = false;

private void OnUIUpdated()
{
    if (_isUpdating) return;
    
    _isUpdating = true;
    try
    {
        Call("AddButton", "Test", ".test");
    }
    finally
    {
        _isUpdating = false;
    }
}

3. Don't forget to unsubscribe

// ❌ Bad - Memory leak potential
public override bool Unload()
{
    return true;  // Forgot to unsubscribe!
}

// ✅ Good
public override bool Unload()
{
    ZUI.OnButtonsChanged -= OnUIUpdated;
    return true;
}

Common Use Cases

Use Case 1: Dynamic Player Count Display

Update UI to show current player count:

private void OnUIUpdated()
{
    RefreshPlayerCount();
}

private void RefreshPlayerCount()
{
    Call("SetPlugin", "PlayerTracker");
    Call("SetTargetWindow", "StatusPanel");
    Call("SetUI", 300, 150);
    
    int playerCount = GetCurrentPlayerCount();
    Call("AddText", $"Players: {playerCount}/50", 20f, 50f);
}

private int GetCurrentPlayerCount()
{
    // Your logic to get player count
    return 24;
}

Use Case 2: Mod Compatibility Check

Detect when other mods register UI:

private HashSet<string> _detectedMods = new HashSet<string>();

private void OnUIUpdated()
{
    CheckForNewMods();
}

private void CheckForNewMods()
{
    // Logic to detect new mod registrations
    var newMod = DetectNewModRegistration();
    
    if (newMod != null && _detectedMods.Add(newMod))
    {
        Log.LogInfo($"Detected new mod with UI: {newMod}");
        UpdateCompatibilityList();
    }
}

Use Case 3: UI State Persistence

Save UI state when changes occur:

private void OnUIUpdated()
{
    SaveUIState();
}

private void SaveUIState()
{
    var state = new UIState
    {
        LastUpdate = DateTime.Now,
        ButtonCount = GetButtonCount(),
        WindowPositions = GetWindowPositions()
    };
    
    // Save to file or config
    SaveToConfig(state);
    Log.LogInfo("UI state saved");
}

Use Case 4: Debug Logging

Track all UI modifications for debugging:

private int _eventCount = 0;

private void OnUIUpdated()
{
    _eventCount++;
    
    Log.LogDebug($"[{_eventCount}] OnButtonsChanged fired");
    Log.LogDebug($"  Time: {DateTime.Now:HH:mm:ss.fff}");
    Log.LogDebug($"  Stack trace: {Environment.StackTrace}");
    
    // Additional debug info
    LogCurrentUIState();
}

private void LogCurrentUIState()
{
    // Log current UI elements, window states, etc.
    Log.LogDebug($"  Current windows: {GetWindowCount()}");
    Log.LogDebug($"  Current buttons: {GetButtonCount()}");
}

Troubleshooting

Event doesn't fire

Possible causes:

  • Event subscription failed
  • ZUI not initialized
  • Wrong event name

Solutions:

Add subscription logging:

private void SubscribeToEvents()
{
    try
    {
        ZUI.OnButtonsChanged += OnUIUpdated;
        Log.LogInfo("✓ Successfully subscribed to OnButtonsChanged");
    }
    catch (Exception ex)
    {
        Log.LogError($"✗ Failed to subscribe: {ex.Message}");
    }
}

Test event manually:

// Trigger event by registering something
Call("AddButton", "Test", ".test");
// Event should fire now

Event fires too many times

Cause: Multiple mods registering UI elements triggers the event multiple times.

Solution: Use rate limiting or debouncing:

private DateTime _lastProcessed = DateTime.MinValue;
private const double MIN_INTERVAL = 0.5;  // 500ms minimum between processing

private void OnUIUpdated()
{
    var elapsed = (DateTime.Now - _lastProcessed).TotalSeconds;
    
    if (elapsed < MIN_INTERVAL)
    {
        Log.LogDebug($"Event ignored (debounced, {elapsed:F2}s since last)");
        return;
    }
    
    _lastProcessed = DateTime.Now;
    ProcessUIUpdate();
}

Memory leak / Performance degradation

Cause: Not unsubscribing from events when mod unloads.

Solution: Always unsubscribe:

public override bool Unload()
{
    if (_eventHandler != null && _onButtonsChangedEvent != null)
    {
        _onButtonsChangedEvent.RemoveEventHandler(null, _eventHandler);
        _eventHandler = null;
        Log.LogInfo("Event handlers cleaned up");
    }
    return true;
}

Exception in event handler

Cause: Unhandled exception in your event handler code.

Solution: Wrap in try-catch:

private void OnUIUpdated()
{
    try
    {
        UpdateMyModUI();
    }
    catch (Exception ex)
    {
        Log.LogError($"Error in OnUIUpdated: {ex.Message}");
        Log.LogError($"Stack trace: {ex.StackTrace}");
    }
}

Complete Example

Here's a complete example using events with soft dependency:

using BepInEx;
using BepInEx.Logging;
using BepInEx.Unity.IL2CPP;
using System;
using System.Linq;
using System.Reflection;

namespace MyMod
{
    [BepInPlugin("com.example.mymod", "My Mod", "1.0.0")]
    [BepInDependency("Zanakinz.ZUI", BepInDependency.DependencyFlags.SoftDependency)]
    public class Plugin : BasePlugin
    {
        private static Type _zuiType;
        private static EventInfo _onButtonsChangedEvent;
        private static Delegate _eventHandler;
        private int _updateCount = 0;

        public override void Load()
        {
            if (InitZUI())
            {
                RegisterUI();
                SubscribeToEvents();
            }
        }

        private bool InitZUI()
        {
            if (!IL2CPPChainloader.Instance.Plugins.ContainsKey("Zanakinz.ZUI"))
                return false;

            var assembly = AppDomain.CurrentDomain.GetAssemblies()
                .FirstOrDefault(a => a.GetName().Name == "ZUI");

            if (assembly == null) return false;

            _zuiType = assembly.GetType("ZUI.API.ZUI");
            if (_zuiType == null) return false;

            _onButtonsChangedEvent = _zuiType.GetEvent("OnButtonsChanged",
                BindingFlags.Public | BindingFlags.Static);

            return _onButtonsChangedEvent != null;
        }

        private void SubscribeToEvents()
        {
            if (_onButtonsChangedEvent == null) return;

            try
            {
                _eventHandler = Delegate.CreateDelegate(
                    _onButtonsChangedEvent.EventHandlerType,
                    this,
                    nameof(OnUIUpdated)
                );

                _onButtonsChangedEvent.AddEventHandler(null, _eventHandler);
                Log.LogInfo("Subscribed to OnButtonsChanged");
            }
            catch (Exception ex)
            {
                Log.LogError($"Event subscription failed: {ex.Message}");
            }
        }

        private void OnUIUpdated()
        {
            try
            {
                _updateCount++;
                Log.LogInfo($"UI updated (event #{_updateCount})");
                
                // Your response logic here
                RefreshUIState();
            }
            catch (Exception ex)
            {
                Log.LogError($"Error in OnUIUpdated: {ex.Message}");
            }
        }

        private void RefreshUIState()
        {
            // Update your UI elements
            Log.LogInfo("Refreshing UI state...");
        }

        private void RegisterUI()
        {
            Call("SetPlugin", "My Mod");
            Call("SetTargetWindow", "Main");
            Call("AddCategory", "Features");
            Call("AddButton", "Test", ".test");
        }

        private void Call(string methodName, params object[] args)
        {
            if (_zuiType == null) return;

            var method = _zuiType.GetMethods(BindingFlags.Public | BindingFlags.Static)
                .FirstOrDefault(m => m.Name == methodName && 
                                     m.GetParameters().Length == args.Length);

            method?.Invoke(null, args);
        }

        public override bool Unload()
        {
            if (_onButtonsChangedEvent != null && _eventHandler != null)
            {
                _onButtonsChangedEvent.RemoveEventHandler(null, _eventHandler);
                Log.LogInfo("Unsubscribed from events");
            }
            return true;
        }
    }
}

Related Pages


Events provide powerful ways to create responsive, dynamic UIs that react to changes in real-time!